1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
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
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
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
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
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
256
257 boolean isRootFolder(final Node folderNode) {
258 return getRootPath().equals(NodeUtil.getPathIfPossible(folderNode));
259 }
260
261
262
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
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
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
416
417
418
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
436
437
438
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
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
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 }