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.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
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
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
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
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
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
247
248 boolean isRootFolder(final Node folderNode) {
249 return getRootPath().equals(NodeUtil.getPathIfPossible(folderNode));
250 }
251
252
253
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
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
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
404
405
406
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
424
425
426
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
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
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 }