001/*
002 * (C) Copyright 2014-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Benoit Delbosc
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.automation.jaxrs.io.documents;
021
022import static org.nuxeo.ecm.core.api.security.SecurityConstants.BROWSE;
023import static org.nuxeo.ecm.core.api.security.SecurityConstants.EVERYONE;
024import static org.nuxeo.ecm.core.api.security.SecurityConstants.UNSUPPORTED_ACL;
025
026import java.io.IOException;
027import java.io.OutputStream;
028import java.lang.annotation.Annotation;
029import java.lang.reflect.Type;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collection;
033import java.util.List;
034import java.util.Map;
035
036import javax.servlet.ServletRequest;
037import javax.ws.rs.Produces;
038import javax.ws.rs.WebApplicationException;
039import javax.ws.rs.core.Context;
040import javax.ws.rs.core.HttpHeaders;
041import javax.ws.rs.core.MediaType;
042import javax.ws.rs.core.MultivaluedMap;
043import javax.ws.rs.ext.MessageBodyWriter;
044import javax.ws.rs.ext.Provider;
045
046import org.apache.commons.lang3.StringUtils;
047import org.nuxeo.ecm.automation.core.util.JSONPropertyWriter;
048import org.nuxeo.ecm.automation.jaxrs.io.JsonHelper;
049import org.nuxeo.ecm.core.api.CoreSession;
050import org.nuxeo.ecm.core.api.DocumentModel;
051import org.nuxeo.ecm.core.api.DocumentRef;
052import org.nuxeo.ecm.core.api.model.Property;
053import org.nuxeo.ecm.core.api.security.ACE;
054import org.nuxeo.ecm.core.api.security.ACL;
055import org.nuxeo.ecm.core.api.security.ACP;
056import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
057import org.nuxeo.ecm.core.io.download.DownloadService;
058import org.nuxeo.ecm.core.schema.SchemaManager;
059import org.nuxeo.ecm.core.security.SecurityService;
060import org.nuxeo.ecm.platform.tag.TagService;
061import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
062import org.nuxeo.runtime.api.Framework;
063
064import com.fasterxml.jackson.core.JsonGenerator;
065
066/**
067 * JSon writer that outputs a format ready to eat by elasticsearch.
068 *
069 * @since 5.9.3
070 */
071@Provider
072@Produces({ JsonESDocumentWriter.MIME_TYPE })
073public class JsonESDocumentWriter implements MessageBodyWriter<DocumentModel> {
074
075    public static final String MIME_TYPE = "application/json+esentity";
076
077    public static final String DOCUMENT_PROPERTIES_HEADER = "X-NXDocumentProperties";
078
079    @Context
080    protected HttpHeaders headers;
081
082    @Override
083    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
084        return DocumentModel.class.isAssignableFrom(type) && MIME_TYPE.equals(mediaType.toString());
085    }
086
087    @Override
088    public long getSize(DocumentModel arg0, Class<?> arg1, Type arg2, Annotation[] arg3, MediaType arg4) {
089        return -1L;
090    }
091
092    @Override
093    public void writeTo(DocumentModel doc, Class<?> type, Type genericType, Annotation[] annotations,
094            MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
095                    throws IOException, WebApplicationException {
096        // schema names: dublincore, file, ... or *
097        List<String> props = headers.getRequestHeader(DOCUMENT_PROPERTIES_HEADER);
098        String[] schemas = null;
099        if (props != null && !props.isEmpty()) {
100            schemas = StringUtils.split(props.get(0), ", ");
101        }
102        writeDocument(entityStream, doc, schemas, null);
103    }
104
105    public void writeDoc(JsonGenerator jg, DocumentModel doc, String[] schemas, Map<String, String> contextParameters,
106            HttpHeaders headers) throws IOException {
107
108        jg.writeStartObject();
109        writeSystemProperties(jg, doc);
110        writeSchemas(jg, doc, schemas);
111        writeContextParameters(jg, doc, contextParameters);
112        jg.writeEndObject();
113        jg.flush();
114    }
115
116    /**
117     * @since 7.2
118     */
119    protected void writeSystemProperties(JsonGenerator jg, DocumentModel doc) throws IOException {
120        String docId = doc.getId();
121        CoreSession session = doc.getCoreSession();
122        jg.writeStringField("ecm:repository", doc.getRepositoryName());
123        jg.writeStringField("ecm:uuid", docId);
124        jg.writeStringField("ecm:name", doc.getName());
125        jg.writeStringField("ecm:title", doc.getTitle());
126
127        String pathAsString = doc.getPathAsString();
128        jg.writeStringField("ecm:path", pathAsString);
129        if (StringUtils.isNotBlank(pathAsString)) {
130            String[] split = pathAsString.split("/");
131            if (split.length > 0) {
132                for (int i = 1; i < split.length; i++) {
133                    jg.writeStringField("ecm:path@level" + i, split[i]);
134                }
135            }
136            jg.writeNumberField("ecm:path@depth", split.length);
137        }
138
139        jg.writeStringField("ecm:primaryType", doc.getType());
140        DocumentRef parentRef = doc.getParentRef();
141        if (parentRef != null) {
142            jg.writeStringField("ecm:parentId", parentRef.toString());
143        }
144        jg.writeStringField("ecm:currentLifeCycleState", doc.getCurrentLifeCycleState());
145        if (doc.isVersion()) {
146            jg.writeStringField("ecm:versionLabel", doc.getVersionLabel());
147            jg.writeStringField("ecm:versionVersionableId", doc.getVersionSeriesId());
148        }
149        jg.writeBooleanField("ecm:isCheckedIn", !doc.isCheckedOut());
150        jg.writeBooleanField("ecm:isProxy", doc.isProxy());
151        jg.writeBooleanField("ecm:isTrashed", doc.isTrashed());
152        jg.writeBooleanField("ecm:isVersion", doc.isVersion());
153        jg.writeBooleanField("ecm:isLatestVersion", doc.isLatestVersion());
154        jg.writeBooleanField("ecm:isLatestMajorVersion", doc.isLatestMajorVersion());
155        jg.writeArrayFieldStart("ecm:mixinType");
156        for (String facet : doc.getFacets()) {
157            jg.writeString(facet);
158        }
159        jg.writeEndArray();
160        TagService tagService = Framework.getService(TagService.class);
161        if (tagService != null && tagService.supportsTag(session, docId)) {
162            jg.writeArrayFieldStart("ecm:tag");
163            for (String tag : tagService.getTags(session, docId)) {
164                jg.writeString(tag);
165            }
166            jg.writeEndArray();
167        }
168        jg.writeStringField("ecm:changeToken", doc.getChangeToken());
169        Long pos = doc.getPos();
170        if (pos != null) {
171            jg.writeNumberField("ecm:pos", pos.longValue());
172        }
173        // Add a positive ACL only
174        SecurityService securityService = Framework.getService(SecurityService.class);
175        List<String> browsePermissions = new ArrayList<>(Arrays.asList(securityService.getPermissionsToCheck(BROWSE)));
176        ACP acp = doc.getACP();
177        if (acp == null) {
178            acp = new ACPImpl();
179        }
180        jg.writeArrayFieldStart("ecm:acl");
181        outerloop: for (ACL acl : acp.getACLs()) {
182            for (ACE ace : acl.getACEs()) {
183                if (ace.isGranted() && ace.isEffective() && browsePermissions.contains(ace.getPermission())) {
184                    jg.writeString(ace.getUsername());
185                }
186                if (ace.isDenied() && ace.isEffective()) {
187                    if (!EVERYONE.equals(ace.getUsername())) {
188                        jg.writeString(UNSUPPORTED_ACL);
189                    }
190                    break outerloop;
191                }
192            }
193        }
194
195        jg.writeEndArray();
196        Map<String, String> bmap = doc.getBinaryFulltext();
197        if (bmap != null && !bmap.isEmpty()) {
198            for (Map.Entry<String, String> item : bmap.entrySet()) {
199                String value = item.getValue();
200                if (value != null) {
201                    jg.writeStringField("ecm:" + item.getKey(), value);
202                }
203            }
204        }
205    }
206
207    /**
208     * @since 7.2
209     */
210    protected void writeSchemas(JsonGenerator jg, DocumentModel doc, String[] schemas) throws IOException {
211        if (schemas == null || (schemas.length == 1 && "*".equals(schemas[0]))) {
212            schemas = doc.getSchemas();
213        }
214        for (String schema : schemas) {
215            writeProperties(jg, doc, schema, null);
216        }
217    }
218
219    /**
220     * @since 7.2
221     */
222    protected void writeContextParameters(JsonGenerator jg, DocumentModel doc, Map<String, String> contextParameters)
223            throws IOException {
224        if (contextParameters != null && !contextParameters.isEmpty()) {
225            for (Map.Entry<String, String> parameter : contextParameters.entrySet()) {
226                jg.writeStringField(parameter.getKey(), parameter.getValue());
227            }
228        }
229    }
230
231    public void writeDocument(OutputStream out, DocumentModel doc, String[] schemas,
232            Map<String, String> contextParameters) throws IOException {
233        writeDoc(JsonHelper.createJsonGenerator(out), doc, schemas, contextParameters, headers);
234    }
235
236    public void writeESDocument(JsonGenerator jg, DocumentModel doc, String[] schemas,
237            Map<String, String> contextParameters) throws IOException {
238        writeDoc(jg, doc, schemas, contextParameters, null);
239    }
240
241    protected static void writeProperties(JsonGenerator jg, DocumentModel doc, String schema, ServletRequest request)
242            throws IOException {
243        Collection<Property> properties = doc.getPropertyObjects(schema);
244        if (properties.isEmpty()) {
245            return;
246        }
247
248        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
249        String prefix = schemaManager.getSchema(schema).getNamespace().prefix;
250        if (prefix == null || prefix.length() == 0) {
251            prefix = schema;
252        }
253        JSONPropertyWriter writer = JSONPropertyWriter.create().writeNull(false).writeEmpty(false).prefix(prefix);
254
255        if (request != null) {
256            DownloadService downloadService = Framework.getService(DownloadService.class);
257            String blobUrlPrefix = VirtualHostHelper.getBaseURL(request)
258                    + downloadService.getDownloadUrl(doc, null, null) + "/";
259            writer.filesBaseUrl(blobUrlPrefix);
260        }
261
262        for (Property p : properties) {
263            writer.writeProperty(jg, p);
264        }
265    }
266
267}