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.security.JCRSessionOp;
39  import info.magnolia.cms.util.PathUtil;
40  import info.magnolia.context.MgnlContext;
41  import info.magnolia.jcr.util.NodeUtil;
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.i18n.I18n;
49  import info.magnolia.rest.reference.ReferenceContext;
50  import info.magnolia.rest.reference.ReferenceDefinition;
51  import info.magnolia.rest.reference.ReferenceResolver;
52  import info.magnolia.rest.reference.ReferenceResolverDefinition;
53  
54  import java.util.Arrays;
55  import java.util.Collections;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.Optional;
59  import java.util.function.Function;
60  import java.util.function.Predicate;
61  
62  import javax.inject.Inject;
63  import javax.jcr.Item;
64  import javax.jcr.Node;
65  import javax.jcr.NodeIterator;
66  import javax.jcr.RepositoryException;
67  import javax.jcr.Session;
68  import javax.jcr.Workspace;
69  import javax.jcr.query.Query;
70  import javax.jcr.query.QueryResult;
71  import javax.ws.rs.DefaultValue;
72  import javax.ws.rs.GET;
73  import javax.ws.rs.NotFoundException;
74  import javax.ws.rs.Path;
75  import javax.ws.rs.PathParam;
76  import javax.ws.rs.Produces;
77  import javax.ws.rs.QueryParam;
78  import javax.ws.rs.core.MediaType;
79  import javax.ws.rs.core.UriInfo;
80  import javax.ws.rs.ext.ContextResolver;
81  import javax.ws.rs.ext.Providers;
82  
83  import org.apache.commons.lang3.StringUtils;
84  import org.slf4j.Logger;
85  import org.slf4j.LoggerFactory;
86  
87  /**
88   * The JCR Delivery endpoint V2 serves JCR content in a concise JSON format.<br/>
89   * This endpoint can be used in multiple configuration for multiple endpoint.
90   * Content may be pages, components, stories or anything else that is stored in a workspace.
91   *
92   * <p>It offers two methods for consuming content:</p>
93   * <ul>
94   * <li>Reading a single node, by passing a specific node path</li>
95   * <li>Querying for nodes; passing query parameters to leverage pagination, sorting, full-text search, or filters</li>
96   * </ul>
97   * <p>The endpoint behavior can be configured with a {@link info.magnolia.rest.delivery.jcr.v2.JcrDeliveryEndpointDefinition} to match specific workspaces or node types.
98   * Nodes are represented in the JSON output as plain object-graph, resembling the tree-structure of JCR nodes and properties.<br/>
99   * Additionally, UUID references to other workspaces can be resolved and expanded within returned records.</p>
100  */
101 @DynamicPath
102 @Path("/")
103 @I18n
104 public class JcrDeliveryEndpoint extends AbstractEndpoint<JcrDeliveryEndpointDefinition> {
105 
106     private static final Logger log = LoggerFactory.getLogger(JcrDeliveryEndpoint.class);
107 
108     private static final String PATH_PARAM = "path";
109     private static final String NODE_TYPES_PARAM = "nodeTypes";
110     private static final String KEYWORD_PARAM = "q";
111     private static final String ORDER_BY_PARAM = "orderBy";
112     private static final String OFFSET_PARAM = "offset";
113     private static final String LIMIT_PARAM = "limit";
114     private static final String LANGUAGE_PARAM = "lang";
115 
116     private static final List<String> ENDPOINT_PARAMETERS = Collections.unmodifiableList(Arrays.asList(
117             NODE_TYPES_PARAM,
118             LANGUAGE_PARAM,
119             KEYWORD_PARAM,
120             ORDER_BY_PARAM,
121             OFFSET_PARAM,
122             LIMIT_PARAM
123     ));
124 
125     private final ComponentProvider componentProvider;
126 
127     @javax.ws.rs.core.Context
128     private UriInfo uriInfo;
129 
130     @javax.ws.rs.core.Context
131     private Providers providers;
132 
133     @Inject
134     public JcrDeliveryEndpoint(JcrDeliveryEndpointDefinition endpointDefinition, ComponentProvider componentProvider) {
135         super(endpointDefinition);
136         this.componentProvider = componentProvider;
137     }
138 
139     /**
140      * Returns a node including its properties and child nodes down to a certain depth.
141      */
142     @GET
143     @Path("/{path:.*}")
144     @Produces({MediaType.APPLICATION_JSON})
145     public Node readNode(@PathParam(PATH_PARAM) @DefaultValue("/") String path) throws RepositoryException {
146 
147         String workspace = getEndpointDefinition().getWorkspace();
148 
149         List<ReferenceDefinition> references = getEndpointDefinition().getReferences();
150         if (references != null && !references.isEmpty()) {
151             // Init reference context
152             ReferenceContext context = getReferenceContext();
153             Map<Predicate, ReferenceResolver> resolvers = getReferenceResolvers(references);
154             context.setResolvers(resolvers);
155         }
156 
157         return doSessionOperation(getEndpointDefinition().isBypassWorkspaceAcls(), new JCRSessionOp<Node>(workspace) {
158             @Override
159             public Node exec(Session session) throws RepositoryException {
160                 String nodePath = PathUtil.createPath(getEndpointDefinition().getRootPath(), path);
161                 Node node = session.getNode(nodePath);
162 
163                 if (getEndpointDefinition().getNodeTypes() != null && !getEndpointDefinition().getNodeTypes().isEmpty()) {
164                     boolean matchingNodeType = false;
165                     for (String nodeType : getEndpointDefinition().getNodeTypes()) {
166                         if (NodeUtil.isNodeType(node, nodeType)) {
167                             matchingNodeType = true;
168                             break;
169                         }
170                     }
171                     if (!matchingNodeType) {
172                         throw new NotFoundException(String.format("Node '%s' does not match any configured node type", node));
173                     }
174                 }
175 
176                 FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
177                         .childNodeTypes(getEndpointDefinition().getChildNodeTypes())
178                         .depth(getEndpointDefinition().getDepth())
179                         .includeSystemProperties(getEndpointDefinition().isIncludeSystemProperties());
180 
181                 node = decorators.wrapNode(node);
182 
183                 return node;
184             }
185         });
186     }
187 
188     /**
189      * Returns a list of nodes.
190      */
191     @GET
192     @Produces({MediaType.APPLICATION_JSON})
193     public NodesResult queryNodes(@QueryParam(KEYWORD_PARAM) String keyword,
194                                   @QueryParam(ORDER_BY_PARAM) String orderByParam,
195                                   @QueryParam(OFFSET_PARAM) Long offsetParam,
196                                   @QueryParam(LIMIT_PARAM) Long limitParam) throws RepositoryException {
197 
198         String workspace = getEndpointDefinition().getWorkspace();
199 
200         long offset = offsetParam == null ? 0 : offsetParam;
201         long limit = limitParam == null ? getEndpointDefinition().getLimit() : limitParam;
202         List<String> propertiesToOrderBy = StringUtils.isEmpty(orderByParam) ?
203                 Collections.emptyList() :
204                 Arrays.stream(StringUtils.split(orderByParam, ","))
205                         .map(String::trim)
206                         .collect(toList());
207         Map<String, List<String>> filteringConditions = uriInfo.getQueryParameters().entrySet().stream()
208                 .filter(entry -> !ENDPOINT_PARAMETERS.contains(entry.getKey()) && !entry.getValue().isEmpty())
209                 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
210 
211         List<ReferenceDefinition> references = getEndpointDefinition().getReferences();
212         if (references != null && !references.isEmpty()) {
213             // Init reference context
214             ReferenceContext context = getReferenceContext();
215             Map<Predicate, ReferenceResolver> resolvers = getReferenceResolvers(references);
216             context.setResolvers(resolvers);
217         }
218 
219         NodeIterator results = doSessionOperation(getEndpointDefinition().isBypassWorkspaceAcls(), new JCRSessionOp<NodeIterator>(workspace) {
220             @Override
221             public NodeIterator exec(Session session) throws RepositoryException {
222                 Workspace workspaceObj = session.getWorkspace();
223 
224                 Query query = QueryBuilder.inWorkspace(workspaceObj)
225                         .rootPath(getEndpointDefinition().getRootPath())
226                         .nodeTypes(getEndpointDefinition().getNodeTypes())
227                         .keyword(keyword)
228                         .conditions(filteringConditions)
229                         .orderBy(propertiesToOrderBy)
230                         .offset(offset)
231                         .limit(limit)
232                         .build();
233 
234                 QueryResult result = query.execute();
235                 NodeIterator nodeIterator = result.getNodes();
236 
237                 FilteringContentDecoratorBuilder decorators = new FilteringContentDecoratorBuilder()
238                         .childNodeTypes(getEndpointDefinition().getChildNodeTypes())
239                         .depth(getEndpointDefinition().getDepth())
240                         .includeSystemProperties(getEndpointDefinition().isIncludeSystemProperties());
241 
242                 nodeIterator = decorators.wrapNodeIterator(nodeIterator);
243 
244                 return nodeIterator;
245             }
246         });
247         //TODO total number of entries in result will be set later.
248         return new NodesResult(results, 0);
249     }
250 
251     private <R> R doSessionOperation(boolean bypassWorkspaceAcls, MgnlContext.Op<R, RepositoryException> operation) throws RepositoryException {
252         if (bypassWorkspaceAcls) {
253             return MgnlContext.doInSystemContext(operation);
254         }
255 
256         return operation.exec();
257     }
258 
259     private ReferenceContext getReferenceContext() {
260         ContextResolver<ReferenceContext> contextResolver = providers.getContextResolver(ReferenceContext.class, MediaType.WILDCARD_TYPE);
261         return contextResolver.getContext(ReferenceContext.class);
262     }
263 
264     private Map<Predicate, ReferenceResolver> getReferenceResolvers(List<ReferenceDefinition> referenceDefinitions) {
265         return referenceDefinitions.stream()
266                 .filter(definition -> definition.getReferenceResolver() != null && definition.getReferenceResolver().getImplementationClass() != null)
267                 .collect(toMap((Function<ReferenceDefinition, Predicate>) this::instantiatePredicate, this::instantiateResolver));
268     }
269 
270     private Predicate<Item> instantiatePredicate(ReferenceDefinition definition) {
271         return item -> {
272             try {
273                 Node node = item.isNode() ? (Node) item : item.getParent();
274                 boolean matchNodeType = definition.getNodeType() == null || node.isNodeType(definition.getNodeType());
275                 boolean matchItemName = definition.getPropertyName() != null && item.getName().matches(definition.getPropertyName());
276                 return matchNodeType && matchItemName;
277             } catch (RepositoryException e) {
278                 log.warn("Cannot get information from item {}:", item, e);
279             }
280 
281             return false;
282         };
283     }
284 
285     private ReferenceResolver instantiateResolver(ReferenceDefinition definition) {
286         ReferenceResolverDefinition resolverDefinition = definition.getReferenceResolver();
287         ReferenceResolver resolver = componentProvider.newInstance(resolverDefinition.getImplementationClass(), resolverDefinition);
288         return new ReferenceResolverWrapper(resolver, getEndpointDefinition().isIncludeSystemProperties(), getEndpointDefinition().isBypassWorkspaceAcls());
289     }
290 
291     /**
292      * A simple wrapper around the ReferenceResolver, to forward the endpoint settings for returned JCR Nodes
293      * (e.g. inclusion of system properties, bypass of workspace ACLs).
294      */
295     private class ReferenceResolverWrapper implements ReferenceResolver {
296         private final ReferenceResolver resolver;
297         private final boolean includeSystemProperties;
298         private final boolean useSystemContext;
299 
300         ReferenceResolverWrapper(ReferenceResolver resolver, boolean includeSystemProperties, boolean useSystemContext) {
301             this.resolver = resolver;
302             this.includeSystemProperties = includeSystemProperties;
303             this.useSystemContext = useSystemContext;
304         }
305 
306         @Override
307         public Optional<?> resolve(Object value) {
308             Optional<?> resolvedItem;
309             try {
310                 resolvedItem = doSessionOperation(useSystemContext, () -> resolver.resolve(value));
311             } catch (RepositoryException e) {
312                 JcrDeliveryEndpoint.log.warn("Cannot resolve referenced item {}:", value, e);
313                 resolvedItem = Optional.empty();
314             }
315 
316             if (resolvedItem.isPresent() && !includeSystemProperties) {
317                 FilteringContentDecoratorBuilder filteringBuilder = new FilteringContentDecoratorBuilder()
318                         .includeSystemProperties(getEndpointDefinition().isIncludeSystemProperties());
319                 if (resolvedItem.get() instanceof Node) {
320                     return ((Optional<Node>) resolvedItem).map(filteringBuilder::wrapNode);
321                 } else if (resolvedItem.get() instanceof NodeIterator) {
322                     return ((Optional<NodeIterator>) resolvedItem).map(filteringBuilder::wrapNodeIterator);
323                 }
324             }
325 
326             return resolvedItem;
327         }
328     }
329 }