View Javadoc
1   /**
2    * This file Copyright (c) 2013-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.dam.jcr;
35  
36  import static info.magnolia.dam.api.AssetProviderCapability.*;
37  import static info.magnolia.dam.api.AssetQuery.Order.ASCENDING;
38  import static javax.jcr.query.qom.QueryObjectModelConstants.*;
39  
40  import info.magnolia.context.Context;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.dam.api.AbstractAssetProvider;
43  import info.magnolia.dam.api.Asset;
44  import info.magnolia.dam.api.AssetProviderCapability;
45  import info.magnolia.dam.api.AssetQuery;
46  import info.magnolia.dam.api.Folder;
47  import info.magnolia.dam.api.Item;
48  import info.magnolia.dam.api.ItemKey;
49  import info.magnolia.dam.api.PathAwareAssetProvider;
50  import info.magnolia.dam.api.metadata.AssetMetadata;
51  import info.magnolia.dam.api.metadata.DublinCore;
52  import info.magnolia.dam.api.metadata.MagnoliaAssetMetadata;
53  import info.magnolia.dam.core.config.DamCoreConfiguration;
54  import info.magnolia.jcr.RuntimeRepositoryException;
55  import info.magnolia.jcr.util.NodeNameHelper;
56  import info.magnolia.jcr.util.NodeTypes;
57  import info.magnolia.jcr.util.NodeUtil;
58  import info.magnolia.jcr.wrapper.I18nNodeWrapper;
59  import info.magnolia.objectfactory.Components;
60  
61  import java.net.URI;
62  import java.net.URISyntaxException;
63  import java.util.Arrays;
64  import java.util.Collections;
65  import java.util.EnumSet;
66  import java.util.HashMap;
67  import java.util.Iterator;
68  import java.util.Map;
69  import java.util.Optional;
70  import java.util.stream.Collectors;
71  import java.util.stream.StreamSupport;
72  
73  import javax.inject.Inject;
74  import javax.inject.Provider;
75  import javax.jcr.ItemNotFoundException;
76  import javax.jcr.Node;
77  import javax.jcr.RepositoryException;
78  import javax.jcr.Session;
79  import javax.jcr.ValueFactory;
80  import javax.jcr.query.Query;
81  import javax.jcr.query.QueryManager;
82  import javax.jcr.query.QueryResult;
83  import javax.jcr.query.Row;
84  import javax.jcr.query.qom.ChildNodeJoinCondition;
85  import javax.jcr.query.qom.Comparison;
86  import javax.jcr.query.qom.Constraint;
87  import javax.jcr.query.qom.DynamicOperand;
88  import javax.jcr.query.qom.Literal;
89  import javax.jcr.query.qom.Ordering;
90  import javax.jcr.query.qom.PropertyExistence;
91  import javax.jcr.query.qom.QueryObjectModelFactory;
92  import javax.jcr.query.qom.Source;
93  import javax.jcr.query.qom.StaticOperand;
94  
95  import org.apache.commons.collections4.IteratorUtils;
96  import org.apache.commons.collections4.Transformer;
97  import org.apache.commons.lang3.StringUtils;
98  import org.apache.jackrabbit.JcrConstants;
99  import org.slf4j.Logger;
100 import org.slf4j.LoggerFactory;
101 
102 import com.machinezoo.noexception.Exceptions;
103 
104 /**
105  * {@link info.magnolia.dam.api.AssetProvider} that delivers assets for the "dam" workspace.
106  */
107 public class JcrAssetProvider extends AbstractAssetProvider implements PathAwareAssetProvider {
108 
109     private static final Logger log = LoggerFactory.getLogger(JcrAssetProvider.class);
110 
111     protected static final String ASSET_SELECTOR_NAME = "asset";
112     private static final String WILDCARD = "%";
113     private static final JcrItemNodeTypePredicateypePredicate">JcrItemNodeTypePredicate ITEM_NODE_TYPE_PREDICATE = new JcrItemNodeTypePredicate();
114 
115     /*
116      * Mapping of default asset properties ids to jcr properties.
117      */
118     private static final Map<String, String> jcrPropertiesMapper;
119 
120     static {
121         Map<String, String> map = new HashMap<String, String>();
122         map.put("created", "mgnl:created");
123         map.put("lastModified", "mgnl:lastModified");
124         jcrPropertiesMapper = Collections.unmodifiableMap(map);
125     }
126 
127 
128     private String workspaceName = DamConstants.WORKSPACE;
129     private String rootPath = "/";
130 
131     private final DamCoreConfiguration configuration;
132     private final Provider<Context> contextProvider;
133     private final NodeNameHelper nodeNameHelper;
134 
135     @Inject
136     public JcrAssetProvider(final DamCoreConfiguration configuration, Provider<Context> contextProvider, NodeNameHelper nodeNameHelper) {
137         this.configuration = configuration;
138         this.contextProvider = contextProvider;
139         this.nodeNameHelper = nodeNameHelper;
140     }
141 
142     /**
143      * @deprecated since 3.0.2. Use {@link #JcrAssetProvider(info.magnolia.dam.core.config.DamCoreConfiguration, javax.inject.Provider, info.magnolia.jcr.util.NodeNameHelper)} instead.
144      */
145     @Deprecated
146     public JcrAssetProvider(final DamCoreConfiguration configuration) {
147         this(configuration, MgnlContext::getInstance, Components.getComponent(NodeNameHelper.class));
148     }
149 
150     public String getRootPath() {
151         return rootPath;
152     }
153 
154     public void setRootPath(String rootPath) {
155         this.rootPath = rootPath;
156     }
157 
158     public String getWorkspaceName() {
159         return workspaceName;
160     }
161 
162     public void setWorkspaceName(String workspaceName) {
163         this.workspaceName = workspaceName;
164     }
165 
166     @Override
167     protected EnumSet<AssetProviderCapability> setupProviderCapabilities() {
168         return EnumSet.of(query, queryWithProviderSpecificString, hierarchical, rendition, queryWithPagination, queryWithSorting);
169     }
170 
171     @Override
172     public boolean supports(Class<? extends AssetMetadata> metaData) {
173         return (MagnoliaAssetMetadata.class.isAssignableFrom(metaData) || DublinCore.class.isAssignableFrom(metaData));
174     }
175 
176     @Override
177     public Asset getAsset(ItemKey assetKey) throws AssetNotFoundException, IllegalItemKeyException {
178         return createAsset(getNodeByIdentifier(assetKey));
179     }
180 
181     @Override
182     public Asset getAsset(String assetPath) throws PathNotFoundException {
183         return createAsset(Exceptions.wrap().get(() -> getNodeByPath(assetPath)));
184     }
185 
186     /**
187      * @throws IllegalArgumentException if the node does not correspond to an Asset node type.
188      */
189     Asset createAsset(final Node assetNode) {
190         if (AssetNodeTypes.isAsset(assetNode)) {
191             log.debug("Created asset linked to the following node '{}'", NodeUtil.getPathIfPossible(assetNode));
192             return new JcrAsset(this, new I18nNodeWrapper(assetNode));
193         }
194         throw new IllegalArgumentException("Node '" + NodeUtil.getPathIfPossible(assetNode) + "' is not defining an asset but another item type");
195     }
196 
197     @Override
198     public Folder getFolder(final ItemKey folderKey) throws AssetNotFoundException, IllegalItemKeyException {
199         return createFolder(getNodeByIdentifier(folderKey));
200     }
201 
202     @Override
203     public Folder getFolder(final String folderPath) throws PathNotFoundException {
204         return createFolder(Exceptions.wrap().get(() -> getNodeByPath(folderPath)));
205     }
206 
207     /**
208      * @throws IllegalArgumentException if the node does not correspond to a Folder node type.
209      */
210     Folder createFolder(final Node folderNode) {
211         if (AssetNodeTypes.isFolder(folderNode) || isRootFolder(folderNode)) {
212             log.debug("Created folder linked to the following node '{}'", NodeUtil.getPathIfPossible(folderNode));
213             return new JcrFolder(this, folderNode);
214         }
215         throw new IllegalArgumentException("Node '" + NodeUtil.getPathIfPossible(folderNode) + "' is not defining a folder but another item type");
216     }
217 
218     @Override
219     public Item getItem(final ItemKey itemKey) throws AssetNotFoundException, IllegalItemKeyException {
220         return createItem(getNodeByIdentifier(itemKey));
221     }
222 
223     @Override
224     public Item getItem(final String path) throws PathNotFoundException {
225         return createItem(Exceptions.wrap().get(() -> getNodeByPath(path)));
226     }
227 
228     Iterator<Item> getChildren(final Node parentNode) {
229         return StreamSupport.stream(Exceptions.wrap().get(() -> NodeUtil.getNodes(parentNode, ITEM_NODE_TYPE_PREDICATE).spliterator()), false)
230                 .map(new NodeToItemTransformer()::transform)
231                 .iterator();
232     }
233 
234     Item getChild(final String name, final Node parentNode) {
235         return Exceptions.wrap().get(() -> {
236             if (parentNode.hasNode(name)) {
237                 return createItem(parentNode.getNode(name));
238             }
239             return null;
240         });
241     }
242 
243     Folder getParent(final Node itemNode) {
244         return createFolder(Exceptions.wrap().get(itemNode::getParent));
245     }
246 
247     /**
248      * @return true if the {@link Node} is the root folder independently of the node type.
249      */
250     boolean isRootFolder(final Node folderNode) {
251         return getRootPath().equals(NodeUtil.getPathIfPossible(folderNode));
252     }
253 
254     /**
255      * @return relative path of the given node based on the 'root path'.
256      */
257     String getPath(final Node itemNode) {
258         try {
259             return getRootPath().equals("/") ? itemNode.getPath() : StringUtils.removeStart(itemNode.getPath(), getRootPath());
260         } catch (RepositoryException e) {
261             throw new RuntimeRepositoryException(e);
262         } catch (RuntimeException e) {
263             if (e.getCause() instanceof ItemNotFoundException) {
264                 return NodeTypes.Deleted.NAME;
265             } else {
266                 throw e;
267             }
268         }
269     }
270 
271     String getName(final Node itemNode) {
272         return Exceptions.wrap().get(itemNode::getName);
273     }
274 
275     /**
276      * @throws IllegalArgumentException if the node does not correspond to a Folder or an Asset NodeType.
277      */
278     Item createItem(final Node node) {
279         if (ITEM_NODE_TYPE_PREDICATE.isDeleted(node)) {
280             throw new AssetNotFoundException(getItemKey(node));
281         } else if (AssetNodeTypes.isAsset(node)) {
282             return createAsset(node);
283         } else if (AssetNodeTypes.isFolder(node) || isRootFolder(node)) {
284             return createFolder(node);
285         }
286         throw new IllegalArgumentException("Node '" + NodeUtil.getPathIfPossible(node) + "' is not referencing an Folder or an Asset but another item type");
287     }
288 
289     @Override
290     public Folder getRootFolder() {
291         return createFolder(getRootNode());
292     }
293 
294     @Override
295     public Iterator<Item> list(final AssetQuery assetQuery) {
296         return Exceptions.wrap().get(() -> {
297             final Query query = buildQuery(assetQuery);
298             log.debug("Running SQL2 query '{}' against workspace {}.", query.getStatement(), getWorkspaceName());
299 
300             // Limit
301             if (assetQuery.getMaxResults() > 0) {
302                 query.setLimit(assetQuery.getMaxResults());
303             }
304             if (assetQuery.getOffset() > 0) {
305                 query.setOffset(assetQuery.getOffset());
306             }
307 
308             final QueryResult queryResult = query.execute();
309             return IteratorUtils.transformedIterator(queryResult.getRows(), new RowToItemTransformer());
310         });
311     }
312 
313     protected Query buildQuery(AssetQuery assetQuery) throws RepositoryException {
314         final Session session = contextProvider.get().getJCRSession(getWorkspaceName());
315         final ValueFactory valueFactory = session.getValueFactory();
316         final QueryManager manager = session.getWorkspace().getQueryManager();
317         final QueryObjectModelFactory factory = manager.getQOMFactory();
318 
319         PropertyExistence deleted = factory.propertyExistence(ASSET_SELECTOR_NAME, NodeTypes.Deleted.DELETED);
320         Constraint nodeTypesConstraints = factory.not(deleted);
321         DynamicOperand operand = factory.propertyValue(ASSET_SELECTOR_NAME, JcrConstants.JCR_PRIMARYTYPE);
322         if (assetQuery.includesAssets()) {
323             final Literal nodeTypeValue = factory.literal(valueFactory.createValue(AssetNodeTypes.Asset.NAME));
324             nodeTypesConstraints = factory.and(nodeTypesConstraints, factory.comparison(operand, JCR_OPERATOR_EQUAL_TO, nodeTypeValue));
325         }
326         if (assetQuery.includesFolders()) {
327             final Literal nodeTypeValue = factory.literal(valueFactory.createValue(NodeTypes.Folder.NAME));
328             final Comparison comparison = factory.comparison(operand, JCR_OPERATOR_EQUAL_TO, nodeTypeValue);
329             nodeTypesConstraints = factory.or(nodeTypesConstraints, comparison);
330         }
331 
332         Constraint and = nodeTypesConstraints;
333 
334         String rootPath = Optional.ofNullable(assetQuery.getRootFolder())
335                 .map(Folder::getItemKey)
336                 .map(ItemKey::getAssetId)
337                 .map(Exceptions.wrap().function(session::getNodeByIdentifier))
338                 .map(Exceptions.wrap().function(Node::getPath))
339                 .orElseGet(assetQuery::getRootPath);
340 
341         if (rootPath != null) {
342             rootPath = Arrays.stream((StringUtils.split(rootPath, "/")))
343                     .map(Exceptions.wrap().function(nodeNameHelper::getValidatedName))
344                     .collect(Collectors.joining("/", "/", StringUtils.EMPTY));
345 
346             and = factory.and(and, assetQuery.includesDescendants() ? factory.descendantNode(ASSET_SELECTOR_NAME, rootPath) : factory.childNode(ASSET_SELECTOR_NAME, rootPath));
347         }
348 
349         if (assetQuery.getKeywordSearchTerm() != null) {
350             String word = assetQuery.getKeywordSearchTerm().toLowerCase();
351             word = nodeNameHelper.getValidatedName(word);
352             final StaticOperand keyword = factory.literal(valueFactory.createValue(WILDCARD + word + WILDCARD));
353 
354             Constraint fullTextSearch = factory.fullTextSearch(ASSET_SELECTOR_NAME, null, keyword);
355 
356             final DynamicOperand localName = factory.lowerCase(factory.nodeLocalName(ASSET_SELECTOR_NAME));
357             fullTextSearch = factory.or(fullTextSearch, factory.comparison(localName, JCR_OPERATOR_LIKE, keyword));
358 
359             and = factory.and(and, fullTextSearch);
360         }
361 
362         for (Map.Entry<String, Object> entry : assetQuery.getProperties().entrySet()) {
363             DynamicOperand lowerCase = factory.lowerCase(factory.propertyValue(ASSET_SELECTOR_NAME, nodeNameHelper.getValidatedName(entry.getKey())));
364             final Literal literal = factory.literal(valueFactory.createValue(WILDCARD + entry.getValue() + WILDCARD));
365             Constraint comparison = factory.comparison(lowerCase, JCR_OPERATOR_LIKE, literal);
366             and = factory.and(and, comparison);
367         }
368 
369         Ordering[] orderings = assetQuery.getSorters().stream()
370                 .map(Exceptions.wrap().function(orderBy -> {
371                     String jcrPropertyName = orderBy.getPropertyId();
372                     if (JcrAssetProvider.jcrPropertiesMapper.containsKey(jcrPropertyName)) {
373                         jcrPropertyName = JcrAssetProvider.jcrPropertiesMapper.get(orderBy.getPropertyId());
374                     }
375                     final DynamicOperand propertyValue = factory.propertyValue(ASSET_SELECTOR_NAME, jcrPropertyName);
376                     return orderBy.getOrder() == ASCENDING ? factory.ascending(propertyValue) : factory.descending(propertyValue);
377                 }))
378                 .toArray(Ordering[]::new);
379 
380         Source nodeSelector = factory.selector(JcrConstants.NT_BASE, ASSET_SELECTOR_NAME);
381 
382         if (assetQuery.getExtension() != null) {
383             final StaticOperand extension = factory.literal(valueFactory.createValue(assetQuery.getExtension()));
384             final String resourceSelectorName = "resource";
385             final Source resourceNodeSelector = factory.selector(JcrConstants.NT_BASE, resourceSelectorName);
386             operand = factory.propertyValue(resourceSelectorName, "extension");
387             Constraint comparison = factory.comparison(operand, JCR_OPERATOR_EQUAL_TO, extension);
388             and = factory.and(and, comparison);
389             final ChildNodeJoinCondition resource = factory.childNodeJoinCondition(resourceSelectorName, ASSET_SELECTOR_NAME);
390             nodeSelector = factory.join(nodeSelector, resourceNodeSelector, JCR_JOIN_TYPE_INNER, resource);
391         }
392 
393         return factory.createQuery(nodeSelector, and, orderings, null);
394     }
395 
396     public String getLink(final Asset asset) {
397         final String contextPath = MgnlContext.getContextPath();
398         String fileName = asset.getFileName();
399         try {
400             fileName = new URI(null, null, fileName, null).toASCIIString();
401         } catch (URISyntaxException urise) {
402             log.warn("Could not encode the following file name {}", fileName);
403         }
404         return contextPath + configuration.getDownloadPath() + asset.getItemKey().asString() + "/" + fileName;
405     }
406 
407     /**
408      * Return the node based on a {@link ItemKey#getAssetId()} only if the target node is in the scope of the {@link info.magnolia.dam.api.AssetProvider#getRootFolder()}.
409      *
410      * @throws IllegalItemKeyException if the node is found but outside the {@link info.magnolia.dam.api.AssetProvider#getRootFolder()} or in case of {@link RepositoryException}.
411      * @throws AssetNotFoundException if no node exist for {@link ItemKey#getAssetId()}.
412      */
413     private Node getNodeByIdentifier(final ItemKey itemKey) {
414         try {
415             Node node = MgnlContext.getJCRSession(getWorkspaceName()).getNodeByIdentifier(itemKey.getAssetId());
416             if (!isChildOfRoot(node)) {
417                 throw new IllegalItemKeyException(itemKey, this, "ItemKey points to an Asset outside the realm of this provider");
418             }
419             return node;
420         } catch (ItemNotFoundException infe) {
421             throw new AssetNotFoundException(itemKey);
422         } catch (RepositoryException e) {
423             throw new IllegalItemKeyException(itemKey, this, e.getMessage());
424         }
425     }
426 
427     /**
428      * Return the node for a relative path based on the {@link info.magnolia.dam.api.AssetProvider#getRootFolder()} path.
429      * If root path is '/foo/baar' and relPath is 'jeff', node will be search under /foo/baar/jeff'.
430      *
431      * @throws PathNotFoundException in case of {@link RepositoryException} or {@link javax.jcr.PathNotFoundException}.
432      */
433     private Node getNodeByPath(String relPath) throws RepositoryException {
434         final Node rootNode = getRootNode();
435         if (StringUtils.isBlank(relPath)) {
436             return rootNode;
437         }
438         final String path = StringUtils.removeStart(relPath, "/");
439         if (rootNode.hasNode(path)) {
440             return rootNode.getNode(path);
441         }
442         throw new PathNotFoundException(relPath);
443     }
444 
445     private Node getRootNode() {
446         return Exceptions.wrap().get(() -> MgnlContext.getJCRSession(getWorkspaceName()).getNode(getRootPath()));
447     }
448 
449     private boolean isChildOfRoot(Node itemNode) throws RepositoryException {
450         return itemNode.getPath().startsWith(getRootPath());
451     }
452 
453     private ItemKey getItemKey(Node node) {
454         return new ItemKey(this.getIdentifier(), Exceptions.wrap().get(node::getIdentifier));
455     }
456 
457     /**
458      * {@link Transformer} implementation used to transform {@link Node} Iterator to {@link Item} Iterator.
459      */
460     class NodeToItemTransformer implements Transformer<Object, Item> {
461 
462         @Override
463         public Item transform(Object input) {
464             return createItem((Node) input);
465         }
466     }
467 
468     /**
469      * {@link Transformer} implementation used to transform a {@link Row} Iterator to {@link Item} Iterator.
470      */
471     class RowToItemTransformer implements Transformer<Row, Item> {
472 
473         @Override
474         public Item transform(Row input) {
475             return createItem(Exceptions.wrap().get(() -> input.getNode(ASSET_SELECTOR_NAME)));
476         }
477     }
478 }