001/* 002 * (C) Copyright 2014-2016 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 * 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.lang.StringUtils; 047import org.codehaus.jackson.JsonGenerator; 048import org.nuxeo.ecm.automation.core.util.JSONPropertyWriter; 049import org.nuxeo.ecm.automation.jaxrs.io.JsonHelper; 050import org.nuxeo.ecm.core.api.CoreSession; 051import org.nuxeo.ecm.core.api.DocumentModel; 052import org.nuxeo.ecm.core.api.DocumentRef; 053import org.nuxeo.ecm.core.api.model.Property; 054import org.nuxeo.ecm.core.api.security.ACE; 055import org.nuxeo.ecm.core.api.security.ACL; 056import org.nuxeo.ecm.core.api.security.ACP; 057import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 058import org.nuxeo.ecm.core.io.download.DownloadService; 059import org.nuxeo.ecm.core.schema.SchemaManager; 060import org.nuxeo.ecm.core.security.SecurityService; 061import org.nuxeo.ecm.platform.tag.TagService; 062import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 063import org.nuxeo.runtime.api.Framework; 064 065/** 066 * JSon writer that outputs a format ready to eat by elasticsearch. 067 * 068 * @since 5.9.3 069 */ 070@Provider 071@Produces({ JsonESDocumentWriter.MIME_TYPE }) 072public class JsonESDocumentWriter implements MessageBodyWriter<DocumentModel> { 073 074 public static final String MIME_TYPE = "application/json+esentity"; 075 076 public static final String DOCUMENT_PROPERTIES_HEADER = "X-NXDocumentProperties"; 077 078 @Context 079 protected HttpHeaders headers; 080 081 @Override 082 public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { 083 return DocumentModel.class.isAssignableFrom(type) && MIME_TYPE.equals(mediaType.toString()); 084 } 085 086 @Override 087 public long getSize(DocumentModel arg0, Class<?> arg1, Type arg2, Annotation[] arg3, MediaType arg4) { 088 return -1L; 089 } 090 091 @Override 092 public void writeTo(DocumentModel doc, Class<?> type, Type genericType, Annotation[] annotations, 093 MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) 094 throws IOException, WebApplicationException { 095 // schema names: dublincore, file, ... or * 096 List<String> props = headers.getRequestHeader(DOCUMENT_PROPERTIES_HEADER); 097 String[] schemas = null; 098 if (props != null && !props.isEmpty()) { 099 schemas = StringUtils.split(props.get(0), ", "); 100 } 101 writeDocument(entityStream, doc, schemas, null); 102 } 103 104 public void writeDoc(JsonGenerator jg, DocumentModel doc, String[] schemas, Map<String, String> contextParameters, 105 HttpHeaders headers) throws IOException { 106 107 jg.writeStartObject(); 108 writeSystemProperties(jg, doc); 109 writeSchemas(jg, doc, schemas); 110 writeContextParameters(jg, doc, contextParameters); 111 jg.writeEndObject(); 112 jg.flush(); 113 } 114 115 /** 116 * @since 7.2 117 */ 118 protected void writeSystemProperties(JsonGenerator jg, DocumentModel doc) throws IOException { 119 String docId = doc.getId(); 120 CoreSession session = doc.getCoreSession(); 121 jg.writeStringField("ecm:repository", doc.getRepositoryName()); 122 jg.writeStringField("ecm:uuid", docId); 123 jg.writeStringField("ecm:name", doc.getName()); 124 jg.writeStringField("ecm:title", doc.getTitle()); 125 126 String pathAsString = doc.getPathAsString(); 127 jg.writeStringField("ecm:path", pathAsString); 128 if (StringUtils.isNotBlank(pathAsString)) { 129 String[] split = pathAsString.split("/"); 130 if (split.length > 0) { 131 for (int i = 1; i < split.length; i++) { 132 jg.writeStringField("ecm:path@level" + i, split[i]); 133 } 134 } 135 jg.writeNumberField("ecm:path@depth", split.length); 136 } 137 138 jg.writeStringField("ecm:primaryType", doc.getType()); 139 DocumentRef parentRef = doc.getParentRef(); 140 if (parentRef != null) { 141 jg.writeStringField("ecm:parentId", parentRef.toString()); 142 } 143 jg.writeStringField("ecm:currentLifeCycleState", doc.getCurrentLifeCycleState()); 144 jg.writeStringField("ecm:versionLabel", doc.getVersionLabel()); 145 jg.writeBooleanField("ecm:isCheckedIn", !doc.isCheckedOut()); 146 jg.writeBooleanField("ecm:isProxy", doc.isProxy()); 147 jg.writeBooleanField("ecm:isVersion", doc.isVersion()); 148 jg.writeBooleanField("ecm:isLatestVersion", doc.isLatestVersion()); 149 jg.writeBooleanField("ecm:isLatestMajorVersion", doc.isLatestMajorVersion()); 150 jg.writeArrayFieldStart("ecm:mixinType"); 151 for (String facet : doc.getFacets()) { 152 jg.writeString(facet); 153 } 154 jg.writeEndArray(); 155 TagService tagService = Framework.getService(TagService.class); 156 if (tagService != null && tagService.supportsTag(session, docId)) { 157 jg.writeArrayFieldStart("ecm:tag"); 158 for (String tag : tagService.getTags(session, docId)) { 159 jg.writeString(tag); 160 } 161 jg.writeEndArray(); 162 } 163 jg.writeStringField("ecm:changeToken", doc.getChangeToken()); 164 Long pos = doc.getPos(); 165 if (pos != null) { 166 jg.writeNumberField("ecm:pos", pos.longValue()); 167 } 168 // Add a positive ACL only 169 SecurityService securityService = Framework.getService(SecurityService.class); 170 List<String> browsePermissions = new ArrayList<>(Arrays.asList(securityService.getPermissionsToCheck(BROWSE))); 171 ACP acp = doc.getACP(); 172 if (acp == null) { 173 acp = new ACPImpl(); 174 } 175 jg.writeArrayFieldStart("ecm:acl"); 176 outerloop: for (ACL acl : acp.getACLs()) { 177 for (ACE ace : acl.getACEs()) { 178 if (ace.isGranted() && ace.isEffective() && browsePermissions.contains(ace.getPermission())) { 179 jg.writeString(ace.getUsername()); 180 } 181 if (ace.isDenied() && ace.isEffective()) { 182 if (!EVERYONE.equals(ace.getUsername())) { 183 jg.writeString(UNSUPPORTED_ACL); 184 } 185 break outerloop; 186 } 187 } 188 } 189 190 jg.writeEndArray(); 191 Map<String, String> bmap = doc.getBinaryFulltext(); 192 if (bmap != null && !bmap.isEmpty()) { 193 for (Map.Entry<String, String> item : bmap.entrySet()) { 194 String value = item.getValue(); 195 if (value != null) { 196 jg.writeStringField("ecm:" + item.getKey(), value); 197 } 198 } 199 } 200 } 201 202 /** 203 * @since 7.2 204 */ 205 protected void writeSchemas(JsonGenerator jg, DocumentModel doc, String[] schemas) throws IOException { 206 if (schemas == null || (schemas.length == 1 && "*".equals(schemas[0]))) { 207 schemas = doc.getSchemas(); 208 } 209 for (String schema : schemas) { 210 writeProperties(jg, doc, schema, null); 211 } 212 } 213 214 /** 215 * @since 7.2 216 */ 217 protected void writeContextParameters(JsonGenerator jg, DocumentModel doc, Map<String, String> contextParameters) 218 throws IOException { 219 if (contextParameters != null && !contextParameters.isEmpty()) { 220 for (Map.Entry<String, String> parameter : contextParameters.entrySet()) { 221 jg.writeStringField(parameter.getKey(), parameter.getValue()); 222 } 223 } 224 } 225 226 public void writeDocument(OutputStream out, DocumentModel doc, String[] schemas, 227 Map<String, String> contextParameters) throws IOException { 228 writeDoc(JsonHelper.createJsonGenerator(out), doc, schemas, contextParameters, headers); 229 } 230 231 public void writeESDocument(JsonGenerator jg, DocumentModel doc, String[] schemas, 232 Map<String, String> contextParameters) throws IOException { 233 writeDoc(jg, doc, schemas, contextParameters, null); 234 } 235 236 protected static void writeProperties(JsonGenerator jg, DocumentModel doc, String schema, ServletRequest request) 237 throws IOException { 238 Collection<Property> properties = doc.getPropertyObjects(schema); 239 if (properties.isEmpty()) { 240 return; 241 } 242 243 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 244 String prefix = schemaManager.getSchema(schema).getNamespace().prefix; 245 if (prefix == null || prefix.length() == 0) { 246 prefix = schema; 247 } 248 JSONPropertyWriter writer = JSONPropertyWriter.create().writeNull(false).writeEmpty(false).prefix(prefix); 249 250 if (request != null) { 251 DownloadService downloadService = Framework.getService(DownloadService.class); 252 String blobUrlPrefix = VirtualHostHelper.getBaseURL(request) 253 + downloadService.getDownloadUrl(doc, null, null) + "/"; 254 writer.filesBaseUrl(blobUrlPrefix); 255 } 256 257 for (Property p : properties) { 258 writer.writeProperty(jg, p); 259 } 260 } 261 262}