Clover icon

Magnolia REST Content Delivery 2.1

  1. Project Clover database Fri Mar 16 2018 18:21:08 CET
  2. Package info.magnolia.rest.delivery.jcr

File NodeWriter.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart9.png
20% of files have more coverage

Code metrics

52
112
13
1
335
239
49
0.44
8.62
13
3.77

Classes

Class Line # Actions
NodeWriter 84 112 0% 49 31
0.824858882.5%
 

Contributing tests

This file is covered by 22 tests. .

Source view

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;
35   
36    import info.magnolia.jcr.util.PropertyUtil;
37    import info.magnolia.rest.reference.ReferenceContext;
38    import info.magnolia.rest.reference.ReferenceResolver;
39   
40    import java.io.IOException;
41    import java.io.OutputStream;
42    import java.lang.annotation.Annotation;
43    import java.lang.reflect.Type;
44    import java.nio.charset.Charset;
45    import java.util.HashSet;
46    import java.util.Map;
47    import java.util.Optional;
48    import java.util.Set;
49   
50    import javax.jcr.Node;
51    import javax.jcr.NodeIterator;
52    import javax.jcr.Property;
53    import javax.jcr.PropertyIterator;
54    import javax.jcr.PropertyType;
55    import javax.jcr.RepositoryException;
56    import javax.jcr.Value;
57    import javax.json.Json;
58    import javax.json.JsonArrayBuilder;
59    import javax.json.stream.JsonGenerator;
60    import javax.ws.rs.Produces;
61    import javax.ws.rs.WebApplicationException;
62    import javax.ws.rs.core.Context;
63    import javax.ws.rs.core.MediaType;
64    import javax.ws.rs.core.MultivaluedHashMap;
65    import javax.ws.rs.core.MultivaluedMap;
66    import javax.ws.rs.ext.ContextResolver;
67    import javax.ws.rs.ext.MessageBodyWriter;
68    import javax.ws.rs.ext.Provider;
69    import javax.ws.rs.ext.Providers;
70   
71    import org.apache.commons.codec.binary.Base64;
72    import org.apache.commons.io.IOUtils;
73    import org.apache.jackrabbit.JcrConstants;
74    import org.slf4j.Logger;
75    import org.slf4j.LoggerFactory;
76   
77    import com.google.common.collect.Sets;
78   
79    /**
80    * Item content provider.
81    */
82    @Provider
83    @Produces({ MediaType.APPLICATION_JSON })
 
84    public class NodeWriter implements MessageBodyWriter<Node> {
85   
86    private static final Logger log = LoggerFactory.getLogger(NodeWriter.class);
87   
88    private static final String SAME_NAME_SIBLING_PREFIX = "_";
89    private static final String META_PROPERTY_PREFIX = "@";
90    private static final String PATH_PROPERTY = META_PROPERTY_PREFIX + "path";
91    private static final String NAME_PROPERTY = META_PROPERTY_PREFIX + "name";
92    private static final String CHILD_PROPERTY = META_PROPERTY_PREFIX + "nodes";
93    private static final String UUID_PROPERTY = META_PROPERTY_PREFIX + "id";
94    private static final String NODE_TYPE_PROPERTY = META_PROPERTY_PREFIX + "nodeType";
95    private static final Set<String> additionalSpecialProps = Sets.newHashSet(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.JCR_UUID);
96   
97    @Context
98    private Providers providers;
99   
 
100  1 toggle @Override
101    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
102  1 return Node.class.isAssignableFrom(type);
103    }
104   
 
105  0 toggle @Override
106    public long getSize(Node node, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
107    // As of JAX-RS 2.0, the method has been deprecated and the value returned by the method is ignored by a JAX-RS runtime.
108  0 return -1;
109    }
110   
 
111  30 toggle @Override
112    public void writeTo(Node node, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
113    MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
114  30 JsonGenerator jsonGenerator = entityStream instanceof OutputStreamWrapper ?
115    ((OutputStreamWrapper) entityStream).getJsonGenerator() :
116    Json.createGenerator(entityStream);
117   
118  30 MultivaluedMap<String, String> resolvedProperties = new MultivaluedHashMap<>();
119  30 writeNode(jsonGenerator, node, null, true, resolvedProperties, annotations, entityStream, mediaType, httpHeaders);
120   
121  30 if (!OutputStreamWrapper.isWrapped(entityStream)) {
122  18 jsonGenerator.flush();
123  18 jsonGenerator.close();
124    }
125    }
126   
127    // package-private only for instrumentation in tests
 
128  116 toggle void writeNode(JsonGenerator jsonGenerator, Node node, String objectKey, boolean isResolveReference, MultivaluedMap<String, String> resolvedProperties,
129    Annotation[] annotations, OutputStream entityStream, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders) throws IOException {
130  116 try {
131  116 if (objectKey != null) {
132  84 jsonGenerator.writeStartObject(objectKey);
133    } else {
134  32 jsonGenerator.writeStartObject();
135    }
136   
137  116 writeSpecialProperties(jsonGenerator, node);
138   
139  116 PropertyIterator properties = node.getProperties();
140   
141  1052 while (properties.hasNext()) {
142  936 Property property = properties.nextProperty();
143  936 if (isAllowedJcrProperty(property.getName())) {
144  717 writeProperty(jsonGenerator, property, node.hasNode(property.getName()), isResolveReference, resolvedProperties, annotations, entityStream, mediaType, httpHeaders);
145    }
146    }
147   
148  116 NodeIterator children = node.getNodes();
149   
150  116 JsonArrayBuilder childNodeNames = Json.createArrayBuilder();
151  198 while (children.hasNext()) {
152  82 Node child = children.nextNode();
153  82 writeNode(jsonGenerator, child, child.getName(), true, resolvedProperties, annotations, entityStream, mediaType, httpHeaders);
154  82 childNodeNames.add(child.getName());
155    }
156  116 jsonGenerator.write(CHILD_PROPERTY, childNodeNames.build());
157   
158  116 jsonGenerator.writeEnd();
159    } catch (RepositoryException re) {
160  0 log.error(re.getMessage());
161    }
162    }
163   
 
164  936 toggle protected boolean isAllowedJcrProperty(String propertyName) {
165  936 return !additionalSpecialProps.contains(propertyName);
166    }
167   
 
168  717 toggle private void writeProperty(JsonGenerator jsonGenerator, Property property, boolean shouldAddPrefix, boolean isResolveReference, MultivaluedMap<String, String> resolvedProperties,
169    Annotation[] annotations, OutputStream entityStream, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders) throws RepositoryException, IOException {
170  717 String propertyName = (shouldAddPrefix ? SAME_NAME_SIBLING_PREFIX : "") + property.getName();
171   
172  717 Optional<ReferenceResolver> referenceResolver = getReferenceResolver(property);
173   
174  717 if (!property.isMultiple()) {
175  710 String valueAsString = getValueString(property.getValue());
176    // Resolve references
177  710 if (isResolveReference && referenceResolver.isPresent()) {
178  5 Optional<Object> reference = (Optional<Object>) referenceResolver.get().resolve(valueAsString);
179  5 if (canWriteReference(property.getPath(), valueAsString, reference, resolvedProperties)) {
180  5 writeResolvedReference(jsonGenerator, reference.get(), propertyName, true, resolvedProperties, annotations, entityStream, mediaType, httpHeaders);
181    } else {
182  0 jsonGenerator.write(propertyName, valueAsString);
183    }
184    } else {
185  705 jsonGenerator.write(propertyName, valueAsString);
186    }
187    } else {
188  7 jsonGenerator.writeStartArray(propertyName);
189  7 Value[] values = property.getValues();
190   
191  7 Set<String> resolvedValues = new HashSet<>();
192  24 for (int i = 0; i < values.length; i++) {
193  17 String valueAsString = getValueString(values[i]);
194    // Resolve references
195  17 if (isResolveReference && referenceResolver.isPresent()) {
196  8 Optional<Object> reference = (Optional<Object>) referenceResolver.get().resolve(valueAsString);
197  8 if (canWriteReference(property.getPath(), valueAsString, reference, resolvedProperties)) {
198  8 writeResolvedReference(jsonGenerator, reference.get(), null, i > 0, resolvedProperties, annotations, entityStream, mediaType, httpHeaders);
199  8 resolvedValues.add(valueAsString);
200  0 } else if (resolvedValues.contains(valueAsString)) { // There may have a case that node has duplicated property somehow
201  0 writeResolvedReference(jsonGenerator, reference.get(), null, i > 0, resolvedProperties, annotations, entityStream, mediaType, httpHeaders);
202    } else {
203  0 jsonGenerator.write(valueAsString);
204    }
205    } else {
206  9 jsonGenerator.write(valueAsString);
207    }
208    }
209  7 jsonGenerator.writeEnd();
210    }
211    }
212   
 
213  13 toggle private boolean canWriteReference(String propertyPath, String referenceValue, Optional<Object> resolvedReference, MultivaluedMap<String, String> resolvedProperties) {
214  13 if (!resolvedReference.isPresent()) {
215  0 return false;
216    }
217  13 Object object = resolvedReference.get();
218  13 if (object instanceof Node) {
219  4 if (resolvedProperties.containsKey(propertyPath) && resolvedProperties.get(propertyPath).contains(referenceValue)) {
220  0 return false;
221    }
222  4 resolvedProperties.putSingle(propertyPath, referenceValue);
223    }
224  13 return true;
225    }
226   
 
227  717 toggle private Optional<ReferenceResolver> getReferenceResolver(Property property) {
228  717 Optional<ReferenceContext> context = getReferenceContext();
229   
230  717 if (context.isPresent()) {
231    // Reference resolver only supports resolving a property, doesn't support resolving a node yet
232  717 return context.get().getResolvers().entrySet().stream()
233    .filter(entry -> entry.getKey().test(property))
234    .map(Map.Entry::getValue)
235    .findFirst();
236    }
237   
238  0 return Optional.empty();
239    }
240   
 
241  717 toggle private Optional<ReferenceContext> getReferenceContext() {
242  717 if (providers == null) {
243  0 return Optional.empty();
244    }
245    // Init reference context
246  717 ContextResolver<ReferenceContext> contextResolver = providers.getContextResolver(ReferenceContext.class, MediaType.WILDCARD_TYPE);
247  717 ReferenceContext context = contextResolver.getContext(ReferenceContext.class);
248  717 return Optional.ofNullable(context);
249    }
250   
 
251  13 toggle private void writeResolvedReference(JsonGenerator jsonGenerator, Object resolvedObject, String propertyName, boolean prependComma, MultivaluedMap<String, String> resolvedProperties,
252    Annotation[] annotations, OutputStream entityStream, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders) throws IOException, RepositoryException {
253  13 if (resolvedObject instanceof Node) {
254  4 writeNode(jsonGenerator, (Node) resolvedObject, propertyName, false, resolvedProperties, annotations, entityStream, mediaType, httpHeaders);
255  4 return;
256    }
257   
258  9 if (resolvedObject instanceof Property) {
259  3 Property resolvedProperty = (Property) resolvedObject;
260    // Don't write object key when writing reference in a JSON array
261  3 if (propertyName == null) {
262  2 jsonGenerator.write(resolvedProperty.getString());
263    } else {
264  1 jsonGenerator.write(propertyName, resolvedProperty.getString());
265    }
266  3 return;
267    }
268   
269  6 if (resolvedObject instanceof String) {
270    // Don't write object key when writing reference in a JSON array
271  3 if (propertyName == null) {
272  2 jsonGenerator.write(resolvedObject.toString());
273    } else {
274  1 jsonGenerator.write(propertyName, resolvedObject.toString());
275    }
276  3 return;
277    }
278   
279    // Get the corresponding MessageBodyWriter for Object which is returned from Reference Resolver
280  3 MessageBodyWriter writer = providers.getMessageBodyWriter(resolvedObject.getClass(), resolvedObject.getClass(), annotations, mediaType);
281  3 jsonGenerator.flush();
282   
283    // FIXME: Because current version of JSON-P does not support print a key. This will be fixed when upgrading JSON-P to version 1.1
284  3 if (prependComma) {
285  2 entityStream.write(','); // Write comma
286    }
287   
288  3 if (propertyName != null) {
289    // If this property is the first one, the comma is not printed
290  1 String propertyKey = String.format("\"%s\":", propertyName);
291  1 entityStream.write(propertyKey.getBytes(Charset.forName("UTF-8")));
292    }
293   
294  3 writer.writeTo(resolvedObject, resolvedObject.getClass(), resolvedObject.getClass(), annotations, mediaType, httpHeaders, entityStream);
295    }
296   
 
297  116 toggle private void writeSpecialProperties(JsonGenerator jsonGenerator, Node node) throws RepositoryException {
298  116 jsonGenerator.write(NAME_PROPERTY, node.getName());
299  116 jsonGenerator.write(PATH_PROPERTY, node.getPath());
300  116 jsonGenerator.write(UUID_PROPERTY, node.getIdentifier());
301  116 jsonGenerator.write(NODE_TYPE_PROPERTY, node.getPrimaryNodeType().getName());
302    }
303   
 
304  727 toggle private String getValueString(Value value) throws RepositoryException {
305  727 switch (value.getType()) {
306  0 case PropertyType.BINARY:
307  0 return getBinaryString(value);
308  0 case PropertyType.DECIMAL:
309  0 return String.valueOf(value.getDecimal());
310  0 case PropertyType.NAME:
311  0 case PropertyType.URI:
312  309 case PropertyType.DATE:
313  309 return value.getString();
314  418 default:
315  418 return PropertyUtil.getValueString(value);
316    }
317    }
318   
319    /**
320    * Gets a string representation of a {@link Value}.
321    * {@link PropertyType#BINARY} types will be base64 encoded.
322    *
323    * @see Base64#encode(byte[])
324    */
 
325  0 toggle private String getBinaryString(Value propertyValue) throws RepositoryException {
326    // Binary values have to be base64 encoded
327  0 byte[] decodedPropertyValue;
328  0 try {
329  0 decodedPropertyValue = Base64.encodeBase64(IOUtils.toByteArray(propertyValue.getBinary().getStream()));
330    } catch (IOException e) {
331  0 throw new RepositoryException(e);
332    }
333  0 return new String(decodedPropertyValue);
334    }
335    }