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.DocumentModel; 051import org.nuxeo.ecm.core.api.DocumentRef; 052import org.nuxeo.ecm.core.api.model.Property; 053import org.nuxeo.ecm.core.api.security.ACE; 054import org.nuxeo.ecm.core.api.security.ACL; 055import org.nuxeo.ecm.core.api.security.ACP; 056import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 057import org.nuxeo.ecm.core.io.download.DownloadService; 058import org.nuxeo.ecm.core.schema.SchemaManager; 059import org.nuxeo.ecm.core.security.SecurityService; 060import org.nuxeo.ecm.platform.tag.Tag; 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 jg.writeStringField("ecm:repository", doc.getRepositoryName()); 120 jg.writeStringField("ecm:uuid", doc.getId()); 121 jg.writeStringField("ecm:name", doc.getName()); 122 jg.writeStringField("ecm:title", doc.getTitle()); 123 124 String pathAsString = doc.getPathAsString(); 125 jg.writeStringField("ecm:path", pathAsString); 126 if (StringUtils.isNotBlank(pathAsString)) { 127 String[] split = pathAsString.split("/"); 128 if (split.length > 0) { 129 for (int i = 1; i < split.length; i++) { 130 jg.writeStringField("ecm:path@level" + i, split[i]); 131 } 132 } 133 jg.writeNumberField("ecm:path@depth", split.length); 134 } 135 136 jg.writeStringField("ecm:primaryType", doc.getType()); 137 DocumentRef parentRef = doc.getParentRef(); 138 if (parentRef != null) { 139 jg.writeStringField("ecm:parentId", parentRef.toString()); 140 } 141 jg.writeStringField("ecm:currentLifeCycleState", doc.getCurrentLifeCycleState()); 142 jg.writeStringField("ecm:versionLabel", doc.getVersionLabel()); 143 jg.writeBooleanField("ecm:isCheckedIn", !doc.isCheckedOut()); 144 jg.writeBooleanField("ecm:isProxy", doc.isProxy()); 145 jg.writeBooleanField("ecm:isVersion", doc.isVersion()); 146 jg.writeBooleanField("ecm:isLatestVersion", doc.isLatestVersion()); 147 jg.writeBooleanField("ecm:isLatestMajorVersion", doc.isLatestMajorVersion()); 148 jg.writeArrayFieldStart("ecm:mixinType"); 149 for (String facet : doc.getFacets()) { 150 jg.writeString(facet); 151 } 152 jg.writeEndArray(); 153 TagService tagService = Framework.getService(TagService.class); 154 if (tagService != null) { 155 jg.writeArrayFieldStart("ecm:tag"); 156 for (Tag tag : tagService.getDocumentTags(doc.getCoreSession(), doc.getId(), null, true)) { 157 jg.writeString(tag.getLabel()); 158 } 159 jg.writeEndArray(); 160 } 161 jg.writeStringField("ecm:changeToken", doc.getChangeToken()); 162 Long pos = doc.getPos(); 163 if (pos != null) { 164 jg.writeNumberField("ecm:pos", pos); 165 } 166 // Add a positive ACL only 167 SecurityService securityService = Framework.getService(SecurityService.class); 168 List<String> browsePermissions = new ArrayList<String>( 169 Arrays.asList(securityService.getPermissionsToCheck(BROWSE))); 170 ACP acp = doc.getACP(); 171 if (acp == null) { 172 acp = new ACPImpl(); 173 } 174 jg.writeArrayFieldStart("ecm:acl"); 175 outerloop: for (ACL acl : acp.getACLs()) { 176 for (ACE ace : acl.getACEs()) { 177 if (ace.isGranted() && ace.isEffective() && browsePermissions.contains(ace.getPermission())) { 178 jg.writeString(ace.getUsername()); 179 } 180 if (ace.isDenied() && ace.isEffective()) { 181 if (!EVERYONE.equals(ace.getUsername())) { 182 jg.writeString(UNSUPPORTED_ACL); 183 } 184 break outerloop; 185 } 186 } 187 } 188 189 jg.writeEndArray(); 190 Map<String, String> bmap = doc.getBinaryFulltext(); 191 if (bmap != null && !bmap.isEmpty()) { 192 for (Map.Entry<String, String> item : bmap.entrySet()) { 193 String value = item.getValue(); 194 if (value != null) { 195 jg.writeStringField("ecm:" + item.getKey(), value); 196 } 197 } 198 } 199 } 200 201 /** 202 * @since 7.2 203 */ 204 protected void writeSchemas(JsonGenerator jg, DocumentModel doc, String[] schemas) throws IOException { 205 if (schemas == null || (schemas.length == 1 && "*".equals(schemas[0]))) { 206 schemas = doc.getSchemas(); 207 } 208 for (String schema : schemas) { 209 writeProperties(jg, doc, schema, null); 210 } 211 } 212 213 /** 214 * @since 7.2 215 */ 216 protected void writeContextParameters(JsonGenerator jg, DocumentModel doc, Map<String, String> contextParameters) 217 throws IOException { 218 if (contextParameters != null && !contextParameters.isEmpty()) { 219 for (Map.Entry<String, String> parameter : contextParameters.entrySet()) { 220 jg.writeStringField(parameter.getKey(), parameter.getValue()); 221 } 222 } 223 } 224 225 public void writeDocument(OutputStream out, DocumentModel doc, String[] schemas, 226 Map<String, String> contextParameters) throws IOException { 227 writeDoc(JsonHelper.createJsonGenerator(out), doc, schemas, contextParameters, headers); 228 } 229 230 public void writeESDocument(JsonGenerator jg, DocumentModel doc, String[] schemas, 231 Map<String, String> contextParameters) throws IOException { 232 writeDoc(jg, doc, schemas, contextParameters, null); 233 } 234 235 protected static void writeProperties(JsonGenerator jg, DocumentModel doc, String schema, ServletRequest request) 236 throws IOException { 237 Collection<Property> properties = doc.getPropertyObjects(schema); 238 if (properties.isEmpty()) { 239 return; 240 } 241 242 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 243 String prefix = schemaManager.getSchema(schema).getNamespace().prefix; 244 if (prefix == null || prefix.length() == 0) { 245 prefix = schema; 246 } 247 JSONPropertyWriter writer = JSONPropertyWriter.create().writeNull(false).writeEmpty(false).prefix(prefix); 248 249 if (request != null) { 250 DownloadService downloadService = Framework.getService(DownloadService.class); 251 String blobUrlPrefix = 252 VirtualHostHelper.getBaseURL(request) + downloadService.getDownloadUrl(doc, null, null) + "/"; 253 writer.filesBaseUrl(blobUrlPrefix); 254 } 255 256 for (Property p : properties) { 257 writer.writeProperty(jg, p); 258 } 259 } 260 261}