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.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
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
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
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
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
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
249
250 boolean isRootFolder(final Node folderNode) {
251 return getRootPath().equals(NodeUtil.getPathIfPossible(folderNode));
252 }
253
254
255
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
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
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
409
410
411
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
429
430
431
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
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
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 }