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