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