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