/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.stanbol.enhancer.jersey.writers; import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA_TYPE; import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE; import static javax.ws.rs.core.MediaType.WILDCARD_TYPE; import static org.apache.stanbol.enhancer.jersey.utils.RequestPropertiesHelper.REQUEST_PROPERTIES_URI; import static org.apache.stanbol.enhancer.jersey.utils.RequestPropertiesHelper.getOutputContent; import static org.apache.stanbol.enhancer.jersey.utils.RequestPropertiesHelper.getOutputContentParts; import static org.apache.stanbol.enhancer.jersey.utils.RequestPropertiesHelper.getParsedContentURIs; import static org.apache.stanbol.enhancer.jersey.utils.RequestPropertiesHelper.getRdfFormat; import static org.apache.stanbol.enhancer.jersey.utils.RequestPropertiesHelper.isOmitMetadata; import static org.apache.stanbol.enhancer.jersey.utils.RequestPropertiesHelper.isOmitParsedContent; import static org.apache.stanbol.enhancer.servicesapi.helper.ContentItemHelper.getBlob; import static org.apache.stanbol.enhancer.servicesapi.helper.ContentItemHelper.getContentParts; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import org.apache.clerezza.commons.rdf.Graph; import org.apache.clerezza.commons.rdf.IRI; import org.apache.clerezza.rdf.core.serializedform.Serializer; import org.apache.clerezza.rdf.core.serializedform.UnsupportedSerializationFormatException; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.http.HttpEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.MIME; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.content.AbstractContentBody; import org.apache.http.entity.mime.content.ContentBody; import org.apache.http.entity.mime.content.ContentDescriptor; import org.apache.http.entity.mime.content.InputStreamBody; import org.apache.http.message.BasicNameValuePair; import org.apache.stanbol.enhancer.servicesapi.Blob; import org.apache.stanbol.enhancer.servicesapi.ContentItem; import org.apache.stanbol.enhancer.servicesapi.helper.ContentItemHelper; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component @Service(Object.class) @Property(name = "javax.ws.rs", boolValue = true) @Provider public class ContentItemWriter implements MessageBodyWriter<ContentItem> { public static final String CONTENT_ITEM_BOUNDARY; public static final String CONTENT_PARTS_BOUNDERY;; /** * The pool of ASCII chars to be used for generating a multipart boundary. */ private final static char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" .toCharArray(); static { final Random rand = new Random(); final int count = rand.nextInt(11) + 10; // a random size from 10 to 20 StringBuilder randomString = new StringBuilder(count); for (int i = 0; i < count; i++) { randomString.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]); } CONTENT_ITEM_BOUNDARY = "contentItem-"+randomString; CONTENT_PARTS_BOUNDERY = "contentParts-"+randomString; } private static final ContentType MULTIPART_ALTERNATE = ContentType.create("multipart/alternate"); Logger log = LoggerFactory.getLogger(ContentItemWriter.class); /** * The "multipart/*" wilrcard */ private static final MediaType MULTIPART = MediaType.valueOf(MULTIPART_FORM_DATA_TYPE.getType()+"/*"); private static final Charset UTF8 = Charset.forName("UTF-8"); /** * The media type for JSON-LD (<code>application/ld+json</code>) */ private static String APPLICATION_LD_JSON = "application/ld+json"; private static MediaType APPLICATION_LD_JSON_TYPE = MediaType.valueOf(APPLICATION_LD_JSON); private static final MediaType DEFAULT_RDF_FORMAT = new MediaType( APPLICATION_LD_JSON_TYPE.getType(), APPLICATION_LD_JSON_TYPE.getSubtype(), Collections.singletonMap("charset", UTF8.name())); @Reference private Serializer serializer; /** * Default Constructor used by OSGI. This expects that the {@link #serializer} * is injected */ public ContentItemWriter(){}; /** * Creates a {@link ContentItemWriter} by using the parsed Clerezza * {@link Serializer}. Intended to be used by unit tests or when running not * in an OSGI environment. * @param serializer */ public ContentItemWriter(Serializer serializer) { this.serializer = serializer; } @Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return //MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(mediaType) && ContentItem.class.isAssignableFrom(type); } @Override public long getSize(ContentItem t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return -1; } @Override public void writeTo(ContentItem ci, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String,Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { //(0) handle default dataType Map<String,Object> reqProp = ContentItemHelper.getRequestPropertiesContentPart(ci); boolean omitMetadata = isOmitMetadata(reqProp); if(!MULTIPART.isCompatible(mediaType)){ //two possible cases if(!omitMetadata){ // (1) just return the RDF data //(1.a) Backward support for default dataType if no Accept header is set StringBuilder ctb = new StringBuilder(); if (mediaType.isWildcardType() || TEXT_PLAIN_TYPE.isCompatible(mediaType) || APPLICATION_OCTET_STREAM_TYPE.isCompatible(mediaType)) { ctb.append(APPLICATION_LD_JSON); } else { ctb.append(mediaType.getType()).append('/').append(mediaType.getSubtype()); } ctb.append(";charset=").append(UTF8.name()); String contentType = ctb.toString(); httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE, contentType); try { serializer.serialize(entityStream, ci.getMetadata(), contentType); } catch (UnsupportedSerializationFormatException e) { throw new WebApplicationException("The enhancement results " + "cannot be serialized in the requested media type: " + mediaType.toString(),Response.Status.NOT_ACCEPTABLE); } } else { // (2) return a single content part Entry<IRI,Blob> contentPart = getBlob(ci, Collections.singleton(mediaType.toString())); if(contentPart == null){ //no alternate content with the requeste media type throw new WebApplicationException("The requested enhancement chain has not created an " + "version of the parsed content in the reuqest media type " + mediaType.toString(),Response.Status.UNSUPPORTED_MEDIA_TYPE); } else { //found -> stream the content to the client //NOTE: This assumes that the presence of a charset // implies reading/writing character streams String requestedCharset = mediaType.getParameters().get("charset"); String blobCharset = contentPart.getValue().getParameter().get("charset"); Charset readerCharset = blobCharset == null ? UTF8 : Charset.forName(blobCharset); Charset writerCharset = requestedCharset == null ? null : Charset.forName(requestedCharset); if(writerCharset != null && !writerCharset.equals(readerCharset)){ //we need to transcode Reader reader = new InputStreamReader( contentPart.getValue().getStream(),readerCharset); Writer writer = new OutputStreamWriter(entityStream, writerCharset); IOUtils.copy(reader, writer); IOUtils.closeQuietly(reader); } else { //no transcoding if(requestedCharset == null && blobCharset != null){ httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE, mediaType.toString()+"; charset="+blobCharset); } InputStream in = contentPart.getValue().getStream(); IOUtils.copy(in, entityStream); IOUtils.closeQuietly(in); } } } } else { // multipart mime requested! final String charsetName = mediaType.getParameters().get("charset"); final Charset charset = charsetName != null ? Charset.forName(charsetName) : UTF8; MediaType rdfFormat; String rdfFormatString = getRdfFormat(reqProp); if(rdfFormatString == null || rdfFormatString.isEmpty()){ rdfFormat = DEFAULT_RDF_FORMAT; } else { try { rdfFormat = MediaType.valueOf(rdfFormatString); if(rdfFormat.getParameters().get("charset") == null){ //use the charset of the default RDF format rdfFormat = new MediaType( rdfFormat.getType(), rdfFormat.getSubtype(), DEFAULT_RDF_FORMAT.getParameters()); } } catch (IllegalArgumentException e) { throw new WebApplicationException("The specified RDF format '" + rdfFormatString +"' (used to serialize all RDF parts of " + "multipart MIME responses) is not a well formated MIME type", Response.Status.BAD_REQUEST); } } //(1) setting the correct header String contentType = String.format("%s/%s; charset=%s; boundary=%s", mediaType.getType(),mediaType.getSubtype(),charset.toString(),CONTENT_ITEM_BOUNDARY); httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE,contentType); MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create(); entityBuilder.setBoundary(CONTENT_ITEM_BOUNDARY); //HttpMultipart entity = new HttpMultipart("from-data", charset ,CONTENT_ITEM_BOUNDARY); //(2) serialising the metadata if(!isOmitMetadata(reqProp)){ entityBuilder.addPart("metadata", new ClerezzaContentBody( ci.getUri().getUnicodeString(), ci.getMetadata(), rdfFormat)); // entity.addBodyPart(new FormBodyPart("metadata", new ClerezzaContentBody( // ci.getUri().getUnicodeString(), ci.getMetadata(), // rdfFormat))); } //(3) serialising the Content (Bloby) //(3.a) Filter based on parameter List<Entry<IRI,Blob>> includedBlobs = filterBlobs(ci, reqProp); //(3.b) Serialise the filtered if(!includedBlobs.isEmpty()) { Map<String,ContentBody> contentParts = new LinkedHashMap<String,ContentBody>(); for(Entry<IRI,Blob> entry : includedBlobs){ Blob blob = entry.getValue(); ContentType ct = ContentType.create(blob.getMimeType()); String cs = blob.getParameter().get("charset"); if(StringUtils.isNotBlank(cs)){ ct = ct.withCharset(cs); } contentParts.put(entry.getKey().getUnicodeString(), new InputStreamBody(blob.getStream(),ct)); } //add all the blobs entityBuilder.addPart("content", new MultipartContentBody(contentParts, CONTENT_PARTS_BOUNDERY, MULTIPART_ALTERNATE)); } //else no content to include Set<String> includeContentParts = getIncludedContentPartURIs(reqProp); if(includeContentParts != null){ //(4) serialise the Request Properties if(includeContentParts.isEmpty() || includeContentParts.contains( REQUEST_PROPERTIES_URI.getUnicodeString())) { JSONObject object; try { object = toJson(reqProp); } catch (JSONException e) { String message = "Unable to convert Request Properties " + "to JSON (values : "+reqProp+")!"; log.error(message,e); throw new WebApplicationException(message, Response.Status.INTERNAL_SERVER_ERROR); } entityBuilder.addTextBody( REQUEST_PROPERTIES_URI.getUnicodeString(), object.toString(), ContentType.APPLICATION_JSON.withCharset(UTF8)); } //(5) additional RDF metadata stored in contentParts for(Entry<IRI,Graph> entry : getContentParts(ci, Graph.class).entrySet()){ if(includeContentParts.isEmpty() || includeContentParts.contains( entry.getKey())){ entityBuilder.addPart(entry.getKey().getUnicodeString(), new ClerezzaContentBody(null, //no file name entry.getValue(),rdfFormat)); } // else ignore this content part } } entityBuilder.build().writeTo(entityStream); } } /** * @param properties * @return */ private JSONObject toJson(Map<?,?> map) throws JSONException { JSONObject object = new JSONObject(); for(Entry<?,?> entry : map.entrySet()){ Object value = getValue(entry.getValue()); object.put(entry.getKey().toString(),value); } return object; } /** * @param entry * @return * @throws JSONException */ private Object getValue(Object javaValue) throws JSONException { Object value; if(javaValue instanceof Collection<?>){ value = new JSONArray(); for(Object o : (Collection<?>)javaValue){ ((JSONArray)value).put(getValue(o)); } } else if(javaValue instanceof Map<?,?>){ value = toJson((Map<?,?>)javaValue); } else { value = javaValue; } return value; } /** * @param properties * @return */ private Set<String> getIncludedContentPartURIs(Map<String,Object> properties) { Collection<String> ocp = getOutputContentParts(properties); if(ocp == null || ocp.isEmpty()){ return null; } Set<String> includeContentParts = new HashSet<String>(ocp); if(includeContentParts != null){ if(includeContentParts.isEmpty()){ //empty == none includeContentParts = null; } else if (includeContentParts.contains("*")){ // * == all -> empty list includeContentParts = Collections.emptySet(); } } return includeContentParts; } /** * @param ci * @param properties * @return */ private List<Entry<IRI,Blob>> filterBlobs(ContentItem ci, Map<String,Object> properties) { final List<Entry<IRI,Blob>> includedContentPartList; Set<MediaType> includeMediaTypes = getIncludedMediaTypes(properties); if(includeMediaTypes == null){ includedContentPartList = Collections.emptyList(); } else { includedContentPartList = new ArrayList<Map.Entry<IRI,Blob>>(); Set<String> ignoreContentPartUris = getIgnoredContentURIs(properties); nextContentPartEntry: for(Entry<IRI,Blob> entry : getContentParts(ci,Blob.class).entrySet()){ if(!ignoreContentPartUris.contains(entry.getKey().getUnicodeString())){ Blob blob = entry.getValue(); MediaType blobMediaType = MediaType.valueOf(blob.getMimeType()); for(MediaType included : includeMediaTypes) { if(blobMediaType.isCompatible(included)){ includedContentPartList.add(entry); continue nextContentPartEntry; } } } //else ignore this Blob } } return includedContentPartList; } /** * @param properties * @return */ private Set<String> getIgnoredContentURIs(Map<String,Object> properties) { Set<String> ignoreContentPartUris = isOmitParsedContent(properties) ? new HashSet<String>(getParsedContentURIs(properties)) : null; if(ignoreContentPartUris == null){ ignoreContentPartUris = Collections.emptySet(); } return ignoreContentPartUris; } /** * @param properties * @return */ private Set<MediaType> getIncludedMediaTypes(Map<String,Object> properties) throws WebApplicationException { Collection<String> includeMediaTypeStrings = getOutputContent(properties); if(includeMediaTypeStrings == null){ return null; } Set<MediaType> includeMediaTypes = new HashSet<MediaType>(includeMediaTypeStrings.size()); for(String includeString : includeMediaTypeStrings){ if(includeString != null){ includeString = includeString.trim(); if(!includeString.isEmpty()){ if("*".equals(includeString)){ //also support '*' for '*/*' includeMediaTypes.add(WILDCARD_TYPE); } else { try { includeMediaTypes.add(MediaType.valueOf(includeString)); } catch (IllegalArgumentException e){ throw new WebApplicationException("The parsed outputContent " + "parameter " + includeMediaTypeStrings +" contain an " + "illegal formated MediaType!", Response.Status.BAD_REQUEST); } } } } } if(includeMediaTypes.contains(WILDCARD_TYPE)){ includeMediaTypes = Collections.singleton(WILDCARD_TYPE); } return includeMediaTypes; } /** * Supports sending multipart mime as {@link ContentBody}. * @author Rupert Westenthaler * */ private class MultipartContentBody extends AbstractContentBody implements ContentBody,ContentDescriptor { private Map<String,ContentBody> parts; private String boundary; public MultipartContentBody(Map<String,ContentBody> parts, String boundary, ContentType contentType){ super(ContentType.create(contentType.getMimeType(), new BasicNameValuePair("boundary",boundary))); this.parts = parts; this.boundary = boundary; } // @Override // public String getCharset() { // return null; //no charset for multipart parts // } // @Override // public String getMimeType() { // String mime = new StringBuilder(super.getMimeType()).append("; boundary=") // .append(boundary).toString(); // log.info("!!! {}",mime); // return mime; // // } @Override public String getTransferEncoding() { return MIME.ENC_8BIT; } @Override public long getContentLength() { //not known as we would need to count the content length AND //the length of the different mime headers. return -1; } @Override public String getFilename() { return null; } @Override public void writeTo(OutputStream out) throws IOException { MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.setBoundary(boundary); for(Entry<String,ContentBody> part : parts.entrySet()){ builder.addPart(part.getKey(), part.getValue()); } HttpEntity entity = builder.build(); entity.writeTo(out); } } /** * Supports serialised RDF graphs as {@link ContentBody} * @author Rupert Westenthaler * */ private class ClerezzaContentBody extends AbstractContentBody implements ContentBody,ContentDescriptor { private Graph graph; private String charset; private String name; protected ClerezzaContentBody(String name, Graph graph, MediaType mimeType){ super(ContentType.create(new StringBuilder(mimeType.getType()) .append('/').append(mimeType.getSubtype()).toString(), UTF8)); charset = mimeType.getParameters().get("charset"); if(charset == null || charset.isEmpty()){ charset = UTF8.toString(); } this.name = name; this.graph = graph; } @Override public String getCharset() { return charset; } @Override public String getTransferEncoding() { return MIME.ENC_8BIT; } @Override public long getContentLength() { return -1; } @Override public String getFilename() { return name; } @Override public void writeTo(OutputStream out) throws IOException { serializer.serialize(out, graph, getMediaType()+'/'+getSubType()); } } }