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.i18n.I18nContentSupport;
39  import info.magnolia.cms.security.JCRSessionOp;
40  import info.magnolia.cms.util.PathUtil;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.objectfactory.ComponentProvider;
43  import info.magnolia.rest.AbstractEndpoint;
44  import info.magnolia.rest.DynamicPath;
45  import info.magnolia.rest.delivery.jcr.NodesResult;
46  import info.magnolia.rest.delivery.jcr.QueryBuilder;
47  import info.magnolia.rest.delivery.jcr.filter.FilteringContentDecoratorBuilder;
48  import info.magnolia.rest.delivery.jcr.filter.NodeTypesPredicate;
49  import info.magnolia.rest.delivery.jcr.i18n.I18n;
50  import info.magnolia.rest.reference.ReferenceContext;
51  import info.magnolia.rest.reference.ReferenceDefinition;
52  import info.magnolia.rest.reference.ReferenceResolver;
53  import info.magnolia.rest.reference.ReferenceResolverDefinition;
54  
55  import java.util.Arrays;
56  import java.util.Collections;
57  import java.util.List;
58  import java.util.Map;
59  import java.util.Optional;
60  import java.util.function.Function;
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.collections.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 List<String> ENDPOINT_PARAMETERS = Collections.unmodifiableList(Arrays.asList(
120             NODE_TYPES_PARAM,
121             LANGUAGE_PARAM,
122             KEYWORD_PARAM,
123             ORDER_BY_PARAM,
124             OFFSET_PARAM,
125             LIMIT_PARAM
126     ));
127 
128     private final ComponentProvider componentProvider;
129     private final I18nContentSupport i18nContentSupport;
130 
131     @javax.ws.rs.core.Context
132     private UriInfo uriInfo;
133 
134     @javax.ws.rs.core.Context
135     private Providers providers;
136 
137     @Inject
138     public JcrDeliveryEndpoint(JcrDeliveryEndpointDefinition endpointDefinition, ComponentProvider componentProvider, I18nContentSupport i18nContentSupport) {
139         super(endpointDefinition);
140         this.componentProvider = componentProvider;
141         this.i18nContentSupport = i18nContentSupport;
142     }
143 
144     /**
145      * Returns a node including its properties and child nodes down to a certain depth.
146      */
147     @GET
148     @Path("/{path:.*(?<!@nodes)}") // Do not allow path that end with @nodes
149     @Produces({ MediaType.APPLICATION_JSON })
150     public Node readNode(@PathParam(PATH_PARAM) @DefaultValue("/") String path) throws RepositoryException {
151 
152         final JcrDeliveryEndpointDefinition definition = getEndpointDefinition();
153         requireNodeTypes(definition);
154         initReferenceContext(definition.getReferences());
155 
156         return doSessionOperation(definition.isBypassWorkspaceAcls(), new JCRSessionOp<Node>(definition.getWorkspace()) {
157             @Override
158             public Node exec(Session session) throws RepositoryException {
159                 String nodePath = PathUtil.createPath(definition.getRootPath(), path);
160                 Node node = session.getNode(nodePath);
161 
162                 NodeTypesPredicate nodeTypesPredicate = new NodeTypesPredicate(definition.getNodeTypes(), !definition.isStrict());
163                 boolean nodeTypeMatched = nodeTypesPredicate.evaluateTyped(node);
164                 if (!nodeTypeMatched) {
165                     throw new NotFoundException(String.format("Node '%s' does not match any configured node type", node));
166                 }
167 
168                 FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
169                         .childNodeTypes(definition.getChildNodeTypes())
170                         .strict(definition.isStrict())
171                         .depth(definition.getDepth())
172                         .includeSystemProperties(definition.isIncludeSystemProperties());
173 
174                 return decorators.wrapNode(node);
175             }
176         });
177     }
178 
179     /**
180      * Returns an array of nodes under a given parent path.
181      */
182     @GET
183     @Path("/{path:(.*)}@nodes")
184     @Produces({ MediaType.APPLICATION_JSON })
185     public NodeIterator getChildren(@PathParam(PATH_PARAM) @DefaultValue("") String path) throws RepositoryException {
186 
187         final JcrDeliveryEndpointDefinition definition = getEndpointDefinition();
188         requireNodeTypes(definition);
189         initReferenceContext(definition.getReferences());
190 
191         return doSessionOperation(definition.isBypassWorkspaceAcls(), new JCRSessionOp<NodeIterator>(definition.getWorkspace()) {
192             @Override
193             public NodeIterator exec(Session session) throws RepositoryException {
194                 String nodePath = PathUtil.createPath(definition.getRootPath(), path);
195                 Node parent = session.getNode(nodePath);
196 
197                 NodeIterator nodeIterator = new FilteringNodeIterator(parent.getNodes(), new NodeTypesPredicate(definition.getNodeTypes(), !definition.isStrict()));
198 
199                 FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
200                         .childNodeTypes(definition.getChildNodeTypes())
201                         .strict(definition.isStrict())
202                         .depth(definition.getDepth())
203                         .includeSystemProperties(definition.isIncludeSystemProperties());
204 
205                 return decorators.wrapNodeIterator(nodeIterator);
206             }
207         });
208     }
209 
210     /**
211      * Searches for nodes matching specific filters and returns a list of results.
212      */
213     @GET
214     @Produces({ MediaType.APPLICATION_JSON })
215     public NodesResult queryNodes(@QueryParam(KEYWORD_PARAM) String keyword,
216                                   @QueryParam(ORDER_BY_PARAM) String orderByParam,
217                                   @QueryParam(OFFSET_PARAM) Long offsetParam,
218                                   @QueryParam(LIMIT_PARAM) Long limitParam) throws RepositoryException {
219 
220         final JcrDeliveryEndpointDefinition definition = getEndpointDefinition();
221         requireNodeTypes(definition);
222         initReferenceContext(definition.getReferences());
223 
224         long offset = offsetParam == null ? 0 : offsetParam;
225         long limit = limitParam == null ? definition.getLimit() : limitParam;
226         List<String> propertiesToOrderBy = StringUtils.isEmpty(orderByParam) ?
227                 Collections.emptyList() :
228                 Arrays.stream(StringUtils.split(orderByParam, ","))
229                         .map(String::trim)
230                         .collect(toList());
231         Map<String, List<String>> filteringConditions = uriInfo.getQueryParameters().entrySet().stream()
232                 .filter(entry -> !ENDPOINT_PARAMETERS.contains(entry.getKey()) && !entry.getValue().isEmpty())
233                 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
234 
235         NodeIterator results = doSessionOperation(definition.isBypassWorkspaceAcls(), new JCRSessionOp<NodeIterator>(definition.getWorkspace()) {
236             @Override
237             public NodeIterator exec(Session session) throws RepositoryException {
238                 Workspace workspaceObj = session.getWorkspace();
239 
240                 Query query = QueryBuilder.inWorkspace(workspaceObj)
241                         .rootPath(definition.getRootPath())
242                         .strict(definition.isStrict())
243                         .nodeTypes(definition.getNodeTypes())
244                         .keyword(keyword)
245                         .conditions(filteringConditions)
246                         .orderBy(propertiesToOrderBy)
247                         .offset(offset)
248                         .limit(limit)
249                         .build();
250 
251                 QueryResult result = query.execute();
252                 NodeIterator nodeIterator = result.getNodes();
253 
254                 FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
255                         .childNodeTypes(definition.getChildNodeTypes())
256                         .strict(definition.isStrict())
257                         .depth(definition.getDepth())
258                         .includeSystemProperties(definition.isIncludeSystemProperties());
259 
260                 nodeIterator = decorators.wrapNodeIterator(nodeIterator);
261 
262                 return nodeIterator;
263             }
264         });
265         //TODO total number of entries in result will be set later.
266         return new NodesResult(results, 0);
267     }
268 
269     private void requireNodeTypes(JcrDeliveryEndpointDefinition definition) {
270         if (definition.getNodeTypes() == null) {
271             throw new NotFoundException(String.format("%s are configured as null, not serving any content", NODE_TYPES_PARAM));
272         } else if (definition.getNodeTypes().isEmpty()) {
273             throw new NotFoundException(String.format("%s are configured as empty, not serving any content", NODE_TYPES_PARAM));
274         }
275     }
276 
277     private <R> R doSessionOperation(boolean bypassWorkspaceAcls, MgnlContext.Op<R, RepositoryException> operation) throws RepositoryException {
278         if (bypassWorkspaceAcls) {
279             return MgnlContext.doInSystemContext(operation);
280         }
281 
282         return operation.exec();
283     }
284 
285     private void initReferenceContext(List<ReferenceDefinition> references) {
286         if (CollectionUtils.isNotEmpty(references)) {
287             // Assign clean reference context for every request
288             ContextResolver<ReferenceContext> contextResolver = providers.getContextResolver(ReferenceContext.class, MediaType.WILDCARD_TYPE);
289             ReferenceContext context = contextResolver.getContext(ReferenceContext.class);
290 
291             // Instantiate predicates & resolvers
292             Map<Predicate, ReferenceResolver> resolvers = references.stream()
293                     .filter(definition -> definition.getReferenceResolver() != null && definition.getReferenceResolver().getImplementationClass() != null)
294                     .collect(toMap((Function<ReferenceDefinition, Predicate>) this::instantiatePredicate, this::instantiateResolver));
295             context.setResolvers(resolvers);
296         }
297     }
298 
299     private Predicate<Item> instantiatePredicate(ReferenceDefinition definition) {
300         return item -> {
301             try {
302                 Node node = item.isNode() ? (Node) item : item.getParent();
303                 boolean matchNodeType = definition.getNodeType() == null || node.isNodeType(definition.getNodeType());
304                 boolean matchItemName = definition.getPropertyName() != null && item.getName().matches(definition.getPropertyName());
305                 return matchNodeType && matchItemName;
306             } catch (RepositoryException e) {
307                 log.warn("Cannot get information from item {}:", item, e);
308             }
309 
310             return false;
311         };
312     }
313 
314     private ReferenceResolver instantiateResolver(ReferenceDefinition definition) {
315         ReferenceResolverDefinition resolverDefinition = definition.getReferenceResolver();
316         ReferenceResolver resolver = componentProvider.newInstance(resolverDefinition.getImplementationClass(), resolverDefinition);
317         return new ReferenceResolverWrapper(resolver, getEndpointDefinition().isIncludeSystemProperties(), getEndpointDefinition().isBypassWorkspaceAcls(), shouldSupportI18n() ? i18nContentSupport : null);
318     }
319 
320     private boolean shouldSupportI18n() {
321         return !ALL_LANGUAGE_PARAM.equalsIgnoreCase(uriInfo.getQueryParameters().getFirst(LANGUAGE_PARAM));
322     }
323 
324     /**
325      * A simple wrapper around the ReferenceResolver, to forward the endpoint settings for returned JCR Nodes
326      * (e.g. inclusion of system properties, bypass of workspace ACLs).
327      */
328     private class ReferenceResolverWrapper implements ReferenceResolver {
329         private final ReferenceResolver resolver;
330         private final boolean includeSystemProperties;
331         private final boolean useSystemContext;
332         private final I18nContentSupport i18nContentSupport;
333 
334         ReferenceResolverWrapper(ReferenceResolver resolver, boolean includeSystemProperties, boolean useSystemContext, I18nContentSupport i18nContentSupport) {
335             this.resolver = resolver;
336             this.includeSystemProperties = includeSystemProperties;
337             this.useSystemContext = useSystemContext;
338             this.i18nContentSupport = i18nContentSupport;
339         }
340 
341         @Override
342         public Optional<?> resolve(Object value) {
343             Optional<?> resolvedItem;
344             try {
345                 resolvedItem = doSessionOperation(useSystemContext, () -> resolver.resolve(value));
346             } catch (RepositoryException e) {
347                 JcrDeliveryEndpoint.log.warn("Cannot resolve referenced item {}:", value, e);
348                 resolvedItem = Optional.empty();
349             }
350 
351             if (resolvedItem.isPresent() && (!includeSystemProperties || i18nContentSupport != null)) {
352                 FilteringContentDecoratorBuilder filteringBuilder = new FilteringContentDecoratorBuilder()
353                         .depth(-1)
354                         .includeSystemProperties(includeSystemProperties)
355                         .supportI18n(i18nContentSupport);
356                 if (resolvedItem.get() instanceof Node) {
357                     return ((Optional<Node>) resolvedItem).map(filteringBuilder::wrapNode);
358                 } else if (resolvedItem.get() instanceof NodeIterator) {
359                     return ((Optional<NodeIterator>) resolvedItem).map(filteringBuilder::wrapNodeIterator);
360                 }
361             }
362 
363             return resolvedItem;
364         }
365     }
366 }