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.rest.delivery.jcr.v2;
35
36 import static java.util.stream.Collectors.*;
37
38 import info.magnolia.cms.core.version.VersionManager;
39 import info.magnolia.cms.i18n.I18nContentSupport;
40 import info.magnolia.cms.security.JCRSessionOp;
41 import info.magnolia.cms.util.PathUtil;
42 import info.magnolia.context.MgnlContext;
43 import info.magnolia.objectfactory.ComponentProvider;
44 import info.magnolia.rest.AbstractEndpoint;
45 import info.magnolia.rest.DynamicPath;
46 import info.magnolia.rest.delivery.jcr.NodesResult;
47 import info.magnolia.rest.delivery.jcr.QueryBuilder;
48 import info.magnolia.rest.delivery.jcr.filter.FilteringContentDecoratorBuilder;
49 import info.magnolia.rest.delivery.jcr.filter.NodeTypesPredicate;
50 import info.magnolia.rest.delivery.jcr.i18n.I18n;
51 import info.magnolia.rest.reference.ReferenceContext;
52 import info.magnolia.rest.reference.ReferenceDefinition;
53 import info.magnolia.rest.reference.ReferenceResolver;
54 import info.magnolia.rest.reference.ReferenceResolverDefinition;
55
56 import java.util.Arrays;
57 import java.util.Collections;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Optional;
61 import java.util.function.Predicate;
62
63 import javax.inject.Inject;
64 import javax.jcr.Item;
65 import javax.jcr.Node;
66 import javax.jcr.NodeIterator;
67 import javax.jcr.RepositoryException;
68 import javax.jcr.Session;
69 import javax.jcr.Workspace;
70 import javax.jcr.query.Query;
71 import javax.jcr.query.QueryResult;
72 import javax.ws.rs.DefaultValue;
73 import javax.ws.rs.GET;
74 import javax.ws.rs.NotFoundException;
75 import javax.ws.rs.Path;
76 import javax.ws.rs.PathParam;
77 import javax.ws.rs.Produces;
78 import javax.ws.rs.QueryParam;
79 import javax.ws.rs.core.MediaType;
80 import javax.ws.rs.core.UriInfo;
81 import javax.ws.rs.ext.ContextResolver;
82 import javax.ws.rs.ext.Providers;
83
84 import org.apache.commons.collections4.CollectionUtils;
85 import org.apache.commons.lang3.StringUtils;
86 import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
87 import org.slf4j.Logger;
88 import org.slf4j.LoggerFactory;
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104 @DynamicPath
105 @Path("/")
106 @I18n
107 public class JcrDeliveryEndpoint extends AbstractEndpoint<JcrDeliveryEndpointDefinition> {
108
109 private static final Logger log = LoggerFactory.getLogger(JcrDeliveryEndpoint.class);
110
111 private static final String PATH_PARAM = "path";
112 private static final String NODE_TYPES_PARAM = "nodeTypes";
113 private static final String KEYWORD_PARAM = "q";
114 private static final String ORDER_BY_PARAM = "orderBy";
115 private static final String OFFSET_PARAM = "offset";
116 private static final String LIMIT_PARAM = "limit";
117 private static final String LANGUAGE_PARAM = "lang";
118 private static final String ALL_LANGUAGE_PARAM = "all";
119 private static final String VERSION = "version";
120 private static final List<String> ENDPOINT_PARAMETERS = Collections.unmodifiableList(Arrays.asList(
121 NODE_TYPES_PARAM,
122 LANGUAGE_PARAM,
123 KEYWORD_PARAM,
124 ORDER_BY_PARAM,
125 OFFSET_PARAM,
126 LIMIT_PARAM
127 ));
128
129 private final ComponentProvider componentProvider;
130 private final I18nContentSupport i18nContentSupport;
131 private final VersionManager versionManager;
132
133 @javax.ws.rs.core.Context
134 private UriInfo uriInfo;
135
136 @javax.ws.rs.core.Context
137 private Providers providers;
138
139 @Inject
140 public JcrDeliveryEndpoint(final JcrDeliveryEndpointDefinition endpointDefinition, final ComponentProvider componentProvider, final I18nContentSupport i18nContentSupport, final VersionManager versionManager) {
141 super(endpointDefinition);
142 this.componentProvider = componentProvider;
143 this.i18nContentSupport = i18nContentSupport;
144 this.versionManager = versionManager;
145 }
146
147
148
149
150 @GET
151 @Path("/{path:.*(?<!@nodes)}")
152 @Produces({ MediaType.APPLICATION_JSON })
153 public Node readNode(@PathParam(PATH_PARAM) @DefaultValue("/") String path, @QueryParam(VERSION) String version) throws RepositoryException {
154
155 final JcrDeliveryEndpointDefinition definition = getEndpointDefinition();
156 requireNodeTypes(definition);
157 initReferenceContext(definition.getReferences());
158
159 return doSessionOperation(definition.isBypassWorkspaceAcls(), new JCRSessionOp<Node>(definition.getWorkspace()) {
160 @Override
161 public Node exec(Session session) throws RepositoryException {
162 String nodePath = PathUtil.createPath(definition.getRootPath(), path);
163 Node node = session.getNode(nodePath);
164 if (StringUtils.isNotEmpty(version) && isPreviewEndpoint()) {
165 node = versionManager.getVersion(node, version);
166 }
167 NodeTypesPredicateesPredicate.html#NodeTypesPredicate">NodeTypesPredicate nodeTypesPredicate = new NodeTypesPredicate(definition.getNodeTypes(), !definition.isStrict());
168 boolean nodeTypeMatched = nodeTypesPredicate.evaluateTyped(node);
169 if (!nodeTypeMatched) {
170 throw new NotFoundException(String.format("Node '%s' does not match any configured node type", node));
171 }
172
173 FilteringContentDecoratorBuilderentDecoratorBuilder.html#FilteringContentDecoratorBuilder">FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
174 .childNodeTypes(definition.getChildNodeTypes())
175 .strict(definition.isStrict())
176 .depth(definition.getDepth())
177 .includeSystemProperties(definition.isIncludeSystemProperties())
178 .systemProperties(definition.getSystemProperties());
179
180 return decorators.wrapNode(node);
181 }
182 });
183 }
184
185
186
187
188 @GET
189 @Path("/{path:(.*)}@nodes")
190 @Produces({ MediaType.APPLICATION_JSON })
191 public NodeIterator getChildren(@PathParam(PATH_PARAM) @DefaultValue("") String path) throws RepositoryException {
192
193 final JcrDeliveryEndpointDefinition definition = getEndpointDefinition();
194 requireNodeTypes(definition);
195 initReferenceContext(definition.getReferences());
196
197 return doSessionOperation(definition.isBypassWorkspaceAcls(), new JCRSessionOp<NodeIterator>(definition.getWorkspace()) {
198 @Override
199 public NodeIterator exec(Session session) throws RepositoryException {
200 String nodePath = PathUtil.createPath(definition.getRootPath(), path);
201 Node parent = session.getNode(nodePath);
202
203 NodeIterator nodeIterator = new FilteringNodeIterator(parent.getNodes(), new NodeTypesPredicate(definition.getNodeTypes(), !definition.isStrict()));
204
205 FilteringContentDecoratorBuilderentDecoratorBuilder.html#FilteringContentDecoratorBuilder">FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
206 .childNodeTypes(definition.getChildNodeTypes())
207 .strict(definition.isStrict())
208 .depth(definition.getDepth())
209 .includeSystemProperties(definition.isIncludeSystemProperties())
210 .systemProperties(definition.getSystemProperties());
211
212 return decorators.wrapNodeIterator(nodeIterator);
213 }
214 });
215 }
216
217
218
219
220 @GET
221 @Produces({ MediaType.APPLICATION_JSON })
222 public NodesResult queryNodes(@QueryParam(KEYWORD_PARAM) String keyword,
223 @QueryParam(ORDER_BY_PARAM) String orderByParam,
224 @QueryParam(OFFSET_PARAM) Long offsetParam,
225 @QueryParam(LIMIT_PARAM) Long limitParam) throws RepositoryException {
226
227 final JcrDeliveryEndpointDefinition definition = getEndpointDefinition();
228 requireNodeTypes(definition);
229 initReferenceContext(definition.getReferences());
230
231 long offset = offsetParam == null ? 0 : offsetParam;
232 long limit = limitParam == null ? definition.getLimit() : limitParam;
233 List<String> propertiesToOrderBy = StringUtils.isEmpty(orderByParam) ?
234 Collections.emptyList() :
235 Arrays.stream(StringUtils.split(orderByParam, ","))
236 .map(String::trim)
237 .collect(toList());
238 Map<String, List<String>> filteringConditions = uriInfo.getQueryParameters().entrySet().stream()
239 .filter(entry -> !ENDPOINT_PARAMETERS.contains(entry.getKey()) && !entry.getValue().isEmpty())
240 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
241
242 NodeIterator results = doSessionOperation(definition.isBypassWorkspaceAcls(), new JCRSessionOp<NodeIterator>(definition.getWorkspace()) {
243 @Override
244 public NodeIterator exec(Session session) throws RepositoryException {
245 Workspace workspaceObj = session.getWorkspace();
246
247 Query query = QueryBuilder.inWorkspace(workspaceObj)
248 .rootPath(definition.getRootPath())
249 .strict(definition.isStrict())
250 .nodeTypes(definition.getNodeTypes())
251 .keyword(keyword)
252 .conditions(filteringConditions)
253 .orderBy(propertiesToOrderBy)
254 .offset(offset)
255 .limit(limit)
256 .build();
257
258 QueryResult result = query.execute();
259 NodeIterator nodeIterator = result.getNodes();
260
261 FilteringContentDecoratorBuilderentDecoratorBuilder.html#FilteringContentDecoratorBuilder">FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
262 .childNodeTypes(definition.getChildNodeTypes())
263 .strict(definition.isStrict())
264 .depth(definition.getDepth())
265 .includeSystemProperties(definition.isIncludeSystemProperties())
266 .systemProperties(definition.getSystemProperties());
267
268 nodeIterator = decorators.wrapNodeIterator(nodeIterator);
269
270 return nodeIterator;
271 }
272 });
273
274 return new NodesResult(results, 0);
275 }
276
277 private void requireNodeTypes(JcrDeliveryEndpointDefinition definition) {
278 if (definition.getNodeTypes() == null) {
279 throw new NotFoundException(String.format("%s are configured as null, not serving any content", NODE_TYPES_PARAM));
280 } else if (definition.getNodeTypes().isEmpty()) {
281 throw new NotFoundException(String.format("%s are configured as empty, not serving any content", NODE_TYPES_PARAM));
282 }
283 }
284
285 private <R> R doSessionOperation(boolean bypassWorkspaceAcls, MgnlContext.Op<R, RepositoryException> operation) throws RepositoryException {
286 if (bypassWorkspaceAcls) {
287 return MgnlContext.doInSystemContext(operation);
288 }
289
290 return operation.exec();
291 }
292
293 private void initReferenceContext(List<ReferenceDefinition> references) {
294 if (CollectionUtils.isNotEmpty(references)) {
295
296 ContextResolver<ReferenceContext> contextResolver = providers.getContextResolver(ReferenceContext.class, MediaType.WILDCARD_TYPE);
297 ReferenceContext context = contextResolver.getContext(ReferenceContext.class);
298 context.setReferenceDepth(getEndpointDefinition().getReferenceDepth());
299 context.setReferenceRepeat(getEndpointDefinition().isReferenceRepeat());
300
301
302 Map<Predicate, ReferenceResolver> resolvers = references.stream()
303 .filter(definition -> definition.getReferenceResolver() != null && definition.getReferenceResolver().getImplementationClass() != null)
304 .collect(toMap(this::instantiatePredicate, this::instantiateResolver));
305 context.setResolvers(resolvers);
306 }
307 }
308
309 private Predicate<Item> instantiatePredicate(ReferenceDefinition definition) {
310 return item -> {
311 try {
312 Node node = item.isNode() ? (Node) item : item.getParent();
313 boolean matchNodeType = definition.getNodeType() == null || node.isNodeType(definition.getNodeType());
314 boolean matchItemName = definition.getPropertyName() != null && item.getName().matches(definition.getPropertyName());
315 return matchNodeType && matchItemName;
316 } catch (RepositoryException e) {
317 log.warn("Cannot get information from item {}:", item, e);
318 }
319
320 return false;
321 };
322 }
323
324 private ReferenceResolver instantiateResolver(ReferenceDefinition definition) {
325 ReferenceResolverDefinition resolverDefinition = definition.getReferenceResolver();
326 ReferenceResolver resolver = componentProvider.newInstance(resolverDefinition.getImplementationClass(), resolverDefinition);
327 return new ReferenceResolverWrapper(resolver, getEndpointDefinition().isIncludeSystemProperties(), getEndpointDefinition().getSystemProperties(), getEndpointDefinition().isBypassWorkspaceAcls(), shouldSupportI18n() ? i18nContentSupport : null);
328 }
329
330 private boolean shouldSupportI18n() {
331 return !ALL_LANGUAGE_PARAM.equalsIgnoreCase(uriInfo.getQueryParameters().getFirst(LANGUAGE_PARAM));
332 }
333
334 private boolean isPreviewEndpoint() {
335 return uriInfo.getPath().contains("/preview/");
336 }
337
338
339
340
341
342 private class ReferenceResolverWrapper implements ReferenceResolver {
343 private final ReferenceResolver resolver;
344 private final boolean includeSystemProperties;
345 private final List<String> systemProperties;
346 private final boolean useSystemContext;
347 private final I18nContentSupport i18nContentSupport;
348
349 ReferenceResolverWrapper(ReferenceResolver resolver, boolean includeSystemProperties, List<String> systemProperties, boolean useSystemContext, I18nContentSupport i18nContentSupport) {
350 this.resolver = resolver;
351 this.includeSystemProperties = includeSystemProperties;
352 this.systemProperties = systemProperties;
353 this.useSystemContext = useSystemContext;
354 this.i18nContentSupport = i18nContentSupport;
355 }
356
357 @Override
358 public Optional<?> resolve(Object value) {
359 Optional<?> resolvedItem;
360 try {
361
362 resolvedItem = doSessionOperation(useSystemContext, () -> resolver.resolve(value));
363 } catch (RepositoryException e) {
364 JcrDeliveryEndpoint.log.warn("Cannot resolve referenced item {}:", value, e);
365 resolvedItem = Optional.empty();
366 }
367
368 if (resolvedItem.isPresent() && (!includeSystemProperties || i18nContentSupport != null)) {
369 FilteringContentDecoratorBuilderoratorBuilder.html#FilteringContentDecoratorBuilder">FilteringContentDecoratorBuilder filteringBuilder = new FilteringContentDecoratorBuilder()
370 .depth(-1)
371 .includeSystemProperties(includeSystemProperties)
372 .systemProperties(systemProperties)
373 .supportI18n(i18nContentSupport);
374 if (resolvedItem.get() instanceof Node) {
375 return ((Optional<Node>) resolvedItem).map(filteringBuilder::wrapNode);
376 } else if (resolvedItem.get() instanceof NodeIterator) {
377 return ((Optional<NodeIterator>) resolvedItem).map(filteringBuilder::wrapNodeIterator);
378 }
379 }
380
381 return resolvedItem;
382 }
383 }
384 }