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.DateTimeFormat; 049import org.nuxeo.ecm.automation.core.util.JSONPropertyWriter; 050import org.nuxeo.ecm.automation.jaxrs.io.JsonHelper; 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.Tag; 062import org.nuxeo.ecm.platform.tag.TagService; 063import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 064import org.nuxeo.runtime.api.Framework; 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 jg.writeStringField("ecm:repository", doc.getRepositoryName()); 121 jg.writeStringField("ecm:uuid", doc.getId()); 122 jg.writeStringField("ecm:name", doc.getName()); 123 jg.writeStringField("ecm:title", doc.getTitle()); 124 125 String pathAsString = doc.getPathAsString(); 126 jg.writeStringField("ecm:path", pathAsString); 127 if (StringUtils.isNotBlank(pathAsString)) { 128 String[] split = pathAsString.split("/"); 129 if (split.length > 0) { 130 for (int i = 1; i < split.length; i++) { 131 jg.writeStringField("ecm:path@level" + i, split[i]); 132 } 133 } 134 jg.writeNumberField("ecm:path@depth", split.length); 135 } 136 137 jg.writeStringField("ecm:primaryType", doc.getType()); 138 DocumentRef parentRef = doc.getParentRef(); 139 if (parentRef != null) { 140 jg.writeStringField("ecm:parentId", parentRef.toString()); 141 } 142 jg.writeStringField("ecm:currentLifeCycleState", doc.getCurrentLifeCycleState()); 143 jg.writeStringField("ecm:versionLabel", doc.getVersionLabel()); 144 jg.writeBooleanField("ecm:isCheckedIn", !doc.isCheckedOut()); 145 jg.writeBooleanField("ecm:isProxy", doc.isProxy()); 146 jg.writeBooleanField("ecm:isVersion", doc.isVersion()); 147 jg.writeBooleanField("ecm:isLatestVersion", doc.isLatestVersion()); 148 jg.writeBooleanField("ecm:isLatestMajorVersion", doc.isLatestMajorVersion()); 149 jg.writeArrayFieldStart("ecm:mixinType"); 150 for (String facet : doc.getFacets()) { 151 jg.writeString(facet); 152 } 153 jg.writeEndArray(); 154 TagService tagService = Framework.getService(TagService.class); 155 if (tagService != null) { 156 jg.writeArrayFieldStart("ecm:tag"); 157 for (Tag tag : tagService.getDocumentTags(doc.getCoreSession(), doc.getId(), null, true)) { 158 jg.writeString(tag.getLabel()); 159 } 160 jg.writeEndArray(); 161 } 162 jg.writeStringField("ecm:changeToken", doc.getChangeToken()); 163 Long pos = doc.getPos(); 164 if (pos != null) { 165 jg.writeNumberField("ecm:pos", pos); 166 } 167 // Add a positive ACL only 168 SecurityService securityService = Framework.getService(SecurityService.class); 169 List<String> browsePermissions = new ArrayList<String>( 170 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 prefix = prefix + ":"; 249 250 String blobUrlPrefix = null; 251 if (request != null) { 252 DownloadService downloadService = Framework.getService(DownloadService.class); 253 blobUrlPrefix = VirtualHostHelper.getBaseURL(request) + downloadService.getDownloadUrl(doc, null, null) 254 + "/"; 255 } 256 257 for (Property p : properties) { 258 jg.writeFieldName(prefix + p.getField().getName().getLocalName()); 259 JSONPropertyWriter.writePropertyValue(jg, p, DateTimeFormat.W3C, blobUrlPrefix); 260 } 261 } 262 263}