View Javadoc
1   /**
2    * This file Copyright (c) 2017-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.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   * The JCR Delivery endpoint V2 serves JCR content in a concise JSON format.<br/>
92   * This endpoint can be used in multiple configuration for multiple endpoint.
93   * Content may be pages, components, stories or anything else that is stored in a workspace.
94   *
95   * <p>It offers two methods for consuming content:</p>
96   * <ul>
97   * <li>Reading a single node, by passing a specific node path</li>
98   * <li>Querying for nodes; passing query parameters to leverage pagination, sorting, full-text search, or filters</li>
99   * </ul>
100  * <p>The endpoint behavior can be configured with a {@link info.magnolia.rest.delivery.jcr.v2.JcrDeliveryEndpointDefinition} to match specific workspaces or node types.
101  * Nodes are represented in the JSON output as plain object-graph, resembling the tree-structure of JCR nodes and properties.<br/>
102  * Additionally, UUID references to other workspaces can be resolved and expanded within returned records.</p>
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      * Returns a node including its properties and child nodes down to a certain depth.
149      */
150     @GET
151     @Path("/{path:.*(?<!@nodes)}") // Do not allow path that end with @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      * Returns an array of nodes under a given parent path.
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      * Searches for nodes matching specific filters and returns a list of results.
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         //TODO total number of entries in result will be set later.
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             // Assign clean reference context for every request
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             // Instantiate predicates & resolvers
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      * A simple wrapper around the ReferenceResolver, to forward the endpoint settings for returned JCR Nodes
340      * (e.g. inclusion of system properties, bypass of workspace ACLs).
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 }