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.elasticsearch.io;
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.util.ArrayList;
028import java.util.Arrays;
029import java.util.Calendar;
030import java.util.Collection;
031import java.util.List;
032import java.util.Map;
033
034import javax.servlet.ServletRequest;
035
036import org.apache.commons.lang3.StringUtils;
037import org.nuxeo.ecm.automation.core.util.JSONPropertyWriter;
038import org.nuxeo.ecm.core.api.CoreSession;
039import org.nuxeo.ecm.core.api.DocumentModel;
040import org.nuxeo.ecm.core.api.DocumentRef;
041import org.nuxeo.ecm.core.api.NuxeoException;
042import org.nuxeo.ecm.core.api.model.Property;
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.io.download.DownloadService;
048import org.nuxeo.ecm.core.schema.SchemaManager;
049import org.nuxeo.ecm.core.security.SecurityService;
050import org.nuxeo.ecm.platform.tag.TagService;
051import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
052import org.nuxeo.runtime.api.Framework;
053
054import com.fasterxml.jackson.core.JsonGenerator;
055
056/**
057 * JSon writer that outputs a format ready to eat by elasticsearch.
058 *
059 * @since 5.9.3
060 */
061public class JsonESDocumentWriter {
062
063    /**
064     * @since 7.2
065     */
066    protected void writeSystemProperties(JsonGenerator jg, DocumentModel doc) throws IOException {
067        String docId = doc.getId();
068        CoreSession session = doc.getCoreSession();
069        jg.writeStringField("ecm:repository", doc.getRepositoryName());
070        jg.writeStringField("ecm:uuid", docId);
071        jg.writeStringField("ecm:name", doc.getName());
072        jg.writeStringField("ecm:title", doc.getTitle());
073
074        String pathAsString = doc.getPathAsString();
075        jg.writeStringField("ecm:path", pathAsString);
076        if (StringUtils.isNotBlank(pathAsString)) {
077            String[] split = pathAsString.split("/");
078            if (split.length > 0) {
079                for (int i = 1; i < split.length; i++) {
080                    jg.writeStringField("ecm:path@level" + i, split[i]);
081                }
082            }
083            jg.writeNumberField("ecm:path@depth", split.length);
084        }
085
086        jg.writeStringField("ecm:primaryType", doc.getType());
087        DocumentRef parentRef = doc.getParentRef();
088        if (parentRef != null) {
089            jg.writeStringField("ecm:parentId", parentRef.toString());
090        }
091        jg.writeStringField("ecm:currentLifeCycleState", doc.getCurrentLifeCycleState());
092        if (doc.isVersion()) {
093            jg.writeStringField("ecm:versionLabel", doc.getVersionLabel());
094            jg.writeStringField("ecm:versionVersionableId", doc.getVersionSeriesId());
095        }
096        jg.writeBooleanField("ecm:isCheckedIn", !doc.isCheckedOut());
097        jg.writeBooleanField("ecm:isProxy", doc.isProxy());
098        jg.writeBooleanField("ecm:isTrashed", doc.isTrashed());
099        jg.writeBooleanField("ecm:isVersion", doc.isVersion());
100        jg.writeBooleanField("ecm:isLatestVersion", doc.isLatestVersion());
101        jg.writeBooleanField("ecm:isLatestMajorVersion", doc.isLatestMajorVersion());
102        jg.writeBooleanField("ecm:isRecord", doc.isRecord());
103        Calendar retainUntil = doc.getRetainUntil();
104        if (retainUntil != null) {
105            jg.writeStringField("ecm:retainUntil", retainUntil.toInstant().toString());
106        }
107        jg.writeBooleanField("ecm:hasLegalHold", doc.hasLegalHold());
108        jg.writeArrayFieldStart("ecm:mixinType");
109        for (String facet : doc.getFacets()) {
110            jg.writeString(facet);
111        }
112        jg.writeEndArray();
113        TagService tagService = Framework.getService(TagService.class);
114        if (tagService != null && tagService.supportsTag(session, docId)) {
115            jg.writeArrayFieldStart("ecm:tag");
116            for (String tag : tagService.getTags(session, docId)) {
117                jg.writeString(tag);
118            }
119            jg.writeEndArray();
120        }
121        jg.writeStringField("ecm:changeToken", doc.getChangeToken());
122        Long pos = doc.getPos();
123        if (pos != null) {
124            jg.writeNumberField("ecm:pos", pos.longValue());
125        }
126        // Add a positive ACL only
127        SecurityService securityService = Framework.getService(SecurityService.class);
128        List<String> browsePermissions = new ArrayList<>(Arrays.asList(securityService.getPermissionsToCheck(BROWSE)));
129        ACP acp = doc.getACP();
130        if (acp == null) {
131            acp = new ACPImpl();
132        }
133        jg.writeArrayFieldStart("ecm:acl");
134        outerloop: for (ACL acl : acp.getACLs()) {
135            for (ACE ace : acl.getACEs()) {
136                if (ace.isGranted() && ace.isEffective() && browsePermissions.contains(ace.getPermission())) {
137                    jg.writeString(ace.getUsername());
138                }
139                if (ace.isDenied() && ace.isEffective()) {
140                    if (!EVERYONE.equals(ace.getUsername())) {
141                        jg.writeString(UNSUPPORTED_ACL);
142                    }
143                    break outerloop;
144                }
145            }
146        }
147
148        jg.writeEndArray();
149        Map<String, String> bmap = getBinaryFulltext(doc);
150        if (bmap != null && !bmap.isEmpty()) {
151            for (Map.Entry<String, String> item : bmap.entrySet()) {
152                String value = item.getValue();
153                if (value != null) {
154                    jg.writeStringField("ecm:" + item.getKey(), value);
155                }
156            }
157        }
158    }
159
160    // kept separate for easy override
161    protected Map<String, String> getBinaryFulltext(DocumentModel doc) throws IOException {
162        return doc.getBinaryFulltext();
163    }
164
165    /**
166     * @since 7.2
167     */
168    protected void writeSchemas(JsonGenerator jg, DocumentModel doc, String[] schemas) throws IOException {
169        if (schemas == null || (schemas.length == 1 && "*".equals(schemas[0]))) {
170            schemas = doc.getSchemas();
171        }
172        for (String schema : schemas) {
173            writeProperties(jg, doc, schema, null);
174        }
175    }
176
177    /**
178     * @since 7.2
179     */
180    protected void writeContextParameters(JsonGenerator jg, DocumentModel doc, Map<String, String> contextParameters)
181            throws IOException {
182        if (contextParameters != null && !contextParameters.isEmpty()) {
183            for (Map.Entry<String, String> parameter : contextParameters.entrySet()) {
184                jg.writeStringField(parameter.getKey(), parameter.getValue());
185            }
186        }
187    }
188
189    public void writeESDocument(JsonGenerator jg, DocumentModel doc, String[] schemas,
190            Map<String, String> contextParameters) throws IOException {
191        jg.writeStartObject();
192        writeSystemProperties(jg, doc);
193        writeSchemas(jg, doc, schemas);
194        writeContextParameters(jg, doc, contextParameters);
195        jg.writeEndObject();
196        jg.flush();
197    }
198
199    protected static void writeProperties(JsonGenerator jg, DocumentModel doc, String schema, ServletRequest request)
200            throws IOException {
201        Collection<Property> properties = doc.getPropertyObjects(schema);
202        if (properties.isEmpty()) {
203            return;
204        }
205
206        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
207        String prefix = schemaManager.getSchema(schema).getNamespace().prefix;
208        if (prefix == null || prefix.length() == 0) {
209            prefix = schema;
210        }
211        JSONPropertyWriter writer = JSONPropertyWriter.create().writeNull(false).writeEmpty(false).prefix(prefix);
212
213        if (request != null) {
214            DownloadService downloadService = Framework.getService(DownloadService.class);
215            String blobUrlPrefix = VirtualHostHelper.getBaseURL(request)
216                    + downloadService.getDownloadUrl(doc, null, null) + "/";
217            writer.filesBaseUrl(blobUrlPrefix);
218        }
219
220        for (Property p : properties) {
221            try {
222                writer.writeProperty(jg, p);
223            } catch (ClassCastException e) {
224                throw new NuxeoException(
225                        String.format("writing JSON property failed on document: %s for property: %s", doc, p), e);
226            }
227        }
228    }
229
230}