001/*
002 * (C) Copyright 2014 Nuxeo SA (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 */
019package org.nuxeo.ecm.automation.jaxrs.io.documents;
020
021import static org.nuxeo.ecm.core.api.security.SecurityConstants.BROWSE;
022import static org.nuxeo.ecm.core.api.security.SecurityConstants.EVERYONE;
023import static org.nuxeo.ecm.core.api.security.SecurityConstants.UNSUPPORTED_ACL;
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.lang.annotation.Annotation;
028import java.lang.reflect.Type;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.List;
032import java.util.Map;
033
034import javax.ws.rs.Produces;
035import javax.ws.rs.core.HttpHeaders;
036import javax.ws.rs.core.MediaType;
037import javax.ws.rs.ext.Provider;
038
039import org.apache.commons.lang.StringUtils;
040import org.codehaus.jackson.JsonEncoding;
041import org.codehaus.jackson.JsonGenerator;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.DocumentRef;
044import org.nuxeo.ecm.core.api.security.ACE;
045import org.nuxeo.ecm.core.api.security.ACL;
046import org.nuxeo.ecm.core.api.security.ACP;
047import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
048import org.nuxeo.ecm.core.security.SecurityService;
049import org.nuxeo.ecm.platform.tag.Tag;
050import org.nuxeo.ecm.platform.tag.TagService;
051import org.nuxeo.runtime.api.Framework;
052
053/**
054 * JSon writer that outputs a format ready to eat by elasticsearch.
055 *
056 * @since 5.9.3
057 */
058@Provider
059@Produces({ JsonESDocumentWriter.MIME_TYPE })
060// TODO: remove the use of JsonDocumentWriter - refactor the ES marshalling and use nuxeo-core-io marshalling instead
061@SuppressWarnings("deprecation")
062public class JsonESDocumentWriter extends JsonDocumentWriter {
063
064    public static final String MIME_TYPE = "application/json+esentity";
065
066    @Override
067    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
068        return super.isWriteable(type, genericType, annotations, mediaType) && MIME_TYPE.equals(mediaType.toString());
069    }
070
071    public void writeDoc(JsonGenerator jg, DocumentModel doc, String[] schemas, Map<String, String> contextParameters,
072            HttpHeaders headers) throws IOException {
073
074        jg.writeStartObject();
075        writeSystemProperties(jg, doc);
076        writeSchemas(jg, doc, schemas);
077        writeContextParameters(jg, doc, contextParameters);
078        jg.writeEndObject();
079        jg.flush();
080    }
081
082    /**
083     * @since 7.2
084     */
085    protected void writeSystemProperties(JsonGenerator jg, DocumentModel doc) throws IOException {
086        jg.writeStringField("ecm:repository", doc.getRepositoryName());
087        jg.writeStringField("ecm:uuid", doc.getId());
088        jg.writeStringField("ecm:name", doc.getName());
089        jg.writeStringField("ecm:title", doc.getTitle());
090
091        String pathAsString = doc.getPathAsString();
092        jg.writeStringField("ecm:path", pathAsString);
093        if (StringUtils.isNotBlank(pathAsString)) {
094            String[] split = pathAsString.split("/");
095            if (split.length > 0) {
096                for (int i = 1; i < split.length; i++) {
097                    jg.writeStringField("ecm:path.level" + i, split[i]);
098                }
099            }
100            jg.writeNumberField("ecm:path.depth", split.length);
101        }
102
103        jg.writeStringField("ecm:primaryType", doc.getType());
104        DocumentRef parentRef = doc.getParentRef();
105        if (parentRef != null) {
106            jg.writeStringField("ecm:parentId", parentRef.toString());
107        }
108        jg.writeStringField("ecm:currentLifeCycleState", doc.getCurrentLifeCycleState());
109        jg.writeStringField("ecm:versionLabel", doc.getVersionLabel());
110        jg.writeBooleanField("ecm:isCheckedIn", !doc.isCheckedOut());
111        jg.writeBooleanField("ecm:isProxy", doc.isProxy());
112        jg.writeBooleanField("ecm:isVersion", doc.isVersion());
113        jg.writeBooleanField("ecm:isLatestVersion", doc.isLatestVersion());
114        jg.writeBooleanField("ecm:isLatestMajorVersion", doc.isLatestMajorVersion());
115        jg.writeArrayFieldStart("ecm:mixinType");
116        for (String facet : doc.getFacets()) {
117            jg.writeString(facet);
118        }
119        jg.writeEndArray();
120        TagService tagService = Framework.getService(TagService.class);
121        if (tagService != null) {
122            jg.writeArrayFieldStart("ecm:tag");
123            for (Tag tag : tagService.getDocumentTags(doc.getCoreSession(), doc.getId(), null, true)) {
124                jg.writeString(tag.getLabel());
125            }
126            jg.writeEndArray();
127        }
128        jg.writeStringField("ecm:changeToken", doc.getChangeToken());
129        Long pos = doc.getPos();
130        if (pos != null) {
131            jg.writeNumberField("ecm:pos", pos);
132        }
133        // Add a positive ACL only
134        SecurityService securityService = Framework.getService(SecurityService.class);
135        List<String> browsePermissions = new ArrayList<String>(
136                Arrays.asList(securityService.getPermissionsToCheck(BROWSE)));
137        ACP acp = doc.getACP();
138        if (acp == null) {
139            acp = new ACPImpl();
140        }
141        jg.writeArrayFieldStart("ecm:acl");
142        outerloop: for (ACL acl : acp.getACLs()) {
143            for (ACE ace : acl.getACEs()) {
144                if (ace.isGranted() && ace.isEffective() && browsePermissions.contains(ace.getPermission())) {
145                    jg.writeString(ace.getUsername());
146                }
147                if (ace.isDenied() && ace.isEffective()) {
148                    if (!EVERYONE.equals(ace.getUsername())) {
149                        jg.writeString(UNSUPPORTED_ACL);
150                    }
151                    break outerloop;
152                }
153            }
154        }
155
156        jg.writeEndArray();
157        Map<String, String> bmap = doc.getBinaryFulltext();
158        if (bmap != null && !bmap.isEmpty()) {
159            for (Map.Entry<String, String> item : bmap.entrySet()) {
160                String value = item.getValue();
161                if (value != null) {
162                    jg.writeStringField("ecm:" + item.getKey(), value);
163                }
164            }
165        }
166    }
167
168    /**
169     * @since 7.2
170     */
171    protected void writeSchemas(JsonGenerator jg, DocumentModel doc, String[] schemas) throws IOException {
172        if (schemas == null || (schemas.length == 1 && "*".equals(schemas[0]))) {
173            schemas = doc.getSchemas();
174        }
175        for (String schema : schemas) {
176            writeProperties(jg, doc, schema, null);
177        }
178    }
179
180    /**
181     * @since 7.2
182     */
183    protected void writeContextParameters(JsonGenerator jg, DocumentModel doc, Map<String, String> contextParameters)
184            throws IOException {
185        if (contextParameters != null && !contextParameters.isEmpty()) {
186            for (Map.Entry<String, String> parameter : contextParameters.entrySet()) {
187                jg.writeStringField(parameter.getKey(), parameter.getValue());
188            }
189        }
190    }
191
192    @Override
193    public void writeDocument(OutputStream out, DocumentModel doc, String[] schemas,
194            Map<String, String> contextParameters) throws IOException {
195        writeDoc(factory.createJsonGenerator(out, JsonEncoding.UTF8), doc, schemas, contextParameters, headers);
196    }
197
198    public void writeESDocument(JsonGenerator jg, DocumentModel doc, String[] schemas,
199            Map<String, String> contextParameters) throws IOException {
200        writeDoc(jg, doc, schemas, contextParameters, null);
201    }
202}