001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.http.commons.responses; 007 008import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE; 009import static org.apache.jena.riot.Lang.JSONLD; 010import static org.apache.jena.riot.Lang.RDFXML; 011import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; 012import static org.apache.jena.riot.RDFLanguages.getRegisteredLanguages; 013import static org.apache.jena.riot.RDFFormat.RDFXML_PLAIN; 014import static org.apache.jena.riot.RDFFormat.JSONLD_COMPACT_FLAT; 015import static org.apache.jena.riot.RDFFormat.JSONLD_EXPAND_FLAT; 016import static org.apache.jena.riot.RDFFormat.JSONLD_FLATTEN_FLAT; 017import static org.apache.jena.riot.system.StreamRDFWriter.defaultSerialization; 018import static org.apache.jena.riot.system.StreamRDFWriter.getWriterStream; 019import static org.fcrepo.kernel.api.RdfCollectors.toModel; 020import static org.slf4j.LoggerFactory.getLogger; 021import static org.fcrepo.kernel.api.RdfLexicon.RDF_NAMESPACE; 022 023import java.io.OutputStream; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.stream.Collectors; 030import java.util.Optional; 031import java.util.Set; 032import javax.ws.rs.WebApplicationException; 033import javax.ws.rs.core.MediaType; 034import javax.ws.rs.core.StreamingOutput; 035 036import com.google.common.util.concurrent.AbstractFuture; 037import org.apache.jena.riot.RiotException; 038import org.apache.jena.graph.Triple; 039import org.apache.jena.rdf.model.Model; 040import org.apache.jena.rdf.model.NsIterator; 041import org.apache.jena.riot.Lang; 042import org.apache.jena.riot.RDFDataMgr; 043import org.apache.jena.riot.RDFFormat; 044import org.apache.jena.riot.system.StreamRDF; 045import org.fcrepo.kernel.api.RdfStream; 046import org.slf4j.Logger; 047 048/** 049 * Serializes an {@link RdfStream}. 050 * 051 * @author ajs6f 052 * @since Oct 30, 2013 053 */ 054public class RdfStreamStreamingOutput extends AbstractFuture<Void> implements 055 StreamingOutput { 056 057 private static final Logger LOGGER = getLogger(RdfStreamStreamingOutput.class); 058 059 private static final String JSONLD_COMPACTED = "http://www.w3.org/ns/json-ld#compacted"; 060 061 private static final String JSONLD_FLATTENED = "http://www.w3.org/ns/json-ld#flattened"; 062 063 private static final String RDF_TYPE = RDF_NAMESPACE + "type"; 064 065 private final Lang format; 066 067 private final MediaType mediaType; 068 069 private final RdfStream rdfStream; 070 071 private final Map<String, String> namespaces; 072 073 /** 074 * Normal constructor 075 * 076 * @param rdfStream the rdf stream 077 * @param namespaces a namespace mapping 078 * @param mediaType the media type 079 */ 080 public RdfStreamStreamingOutput(final RdfStream rdfStream, final Map<String, String> namespaces, 081 final MediaType mediaType) { 082 super(); 083 084 if (LOGGER.isDebugEnabled()) { 085 getRegisteredLanguages().forEach(format -> { 086 LOGGER.debug("Discovered RDF writer writeableFormats: {} with mimeTypes: {}", 087 format.getName(), String.join(" ", format.getAltContentTypes())); 088 }); 089 } 090 final Lang format = contentTypeToLang(mediaType.getType() + "/" + mediaType.getSubtype()); 091 if (format != null) { 092 this.format = format; 093 this.mediaType = mediaType; 094 LOGGER.debug("Setting up to serialize to: {}", format); 095 } else { 096 throw new WebApplicationException(NOT_ACCEPTABLE); 097 } 098 099 this.rdfStream = rdfStream; 100 this.namespaces = namespaces; 101 } 102 103 @Override 104 public void write(final OutputStream output) { 105 try { 106 LOGGER.debug("Serializing RDF stream in: {}", format); 107 write(rdfStream, output, format, mediaType, namespaces); 108 } catch (final RiotException e) { 109 setException(e); 110 LOGGER.debug("Error serializing RDF: {}", e.getMessage()); 111 throw new WebApplicationException(e); 112 } 113 } 114 115 private static void write(final RdfStream rdfStream, 116 final OutputStream output, 117 final Lang dataFormat, 118 final MediaType dataMediaType, 119 final Map<String, String> nsPrefixes) { 120 121 final RDFFormat format = defaultSerialization(dataFormat); 122 123 // For formats that can be block-streamed (n-triples, turtle) 124 if (format != null) { 125 LOGGER.debug("Stream-based serialization of {}", dataFormat.toString()); 126 if (RDFFormat.NTRIPLES.equals(format)) { 127 serializeNTriples(rdfStream, format, output); 128 } else { 129 serializeBlockStreamed(rdfStream, output, format, nsPrefixes); 130 } 131 // For formats that require analysis of the entire model and cannot be streamed directly (rdfxml, n3) 132 } else { 133 LOGGER.debug("Non-stream serialization of {}", dataFormat.toString()); 134 serializeNonStreamed(rdfStream, output, dataFormat, dataMediaType, nsPrefixes); 135 } 136 } 137 138 private static void serializeNTriples(final RdfStream rdfStream, final RDFFormat format, 139 final OutputStream output) { 140 final StreamRDF stream = new SynchonizedStreamRDFWrapper(getWriterStream(output, format.getLang())); 141 stream.start(); 142 rdfStream.forEach(stream::triple); 143 stream.finish(); 144 } 145 146 private static void serializeBlockStreamed(final RdfStream rdfStream, final OutputStream output, 147 final RDFFormat format, final Map<String, String> nsPrefixes) { 148 149 final Set<String> namespacesPresent = new HashSet<>(); 150 151 final StreamRDF stream = new SynchonizedStreamRDFWrapper(getWriterStream(output, format.getLang())); 152 stream.start(); 153 // Must read the rdf stream before writing out ns prefixes, otherwise the prefixes come after the triples 154 final List<Triple> tripleList = rdfStream.peek(t -> { 155 // Collect the namespaces present in the RDF stream, using the same 156 // criteria for where to look that jena's model.listNameSpaces() does 157 namespacesPresent.add(t.getPredicate().getNameSpace()); 158 if (RDF_TYPE.equals(t.getPredicate().getURI()) && t.getObject().isURI()) { 159 namespacesPresent.add(t.getObject().getNameSpace()); 160 } 161 }).collect(Collectors.toList()); 162 163 nsPrefixes.forEach((prefix, uri) -> { 164 // Only add namespace prefixes if the namespace is present in the rdf stream 165 if (namespacesPresent.contains(uri)) { 166 stream.prefix(prefix, uri); 167 } 168 }); 169 tripleList.forEach(stream::triple); 170 stream.finish(); 171 } 172 173 private static void serializeNonStreamed(final RdfStream rdfStream, final OutputStream output, 174 final Lang dataFormat, final MediaType dataMediaType, final Map<String, String> nsPrefixes) { 175 final Model model = rdfStream.collect(toModel()); 176 177 model.setNsPrefixes(filterNamespacesToPresent(model, nsPrefixes)); 178 // use block output streaming for RDFXML 179 if (RDFXML.equals(dataFormat)) { 180 RDFDataMgr.write(output, model.getGraph(), RDFXML_PLAIN); 181 } else if (JSONLD.equals(dataFormat)) { 182 final RDFFormat jsonldFormat = getFormatFromMediaType(dataMediaType); 183 RDFDataMgr.write(output, model.getGraph(), jsonldFormat); 184 } else { 185 RDFDataMgr.write(output, model.getGraph(), dataFormat); 186 } 187 } 188 189 /** 190 * Filters the map of namespace prefix mappings to just those containing namespace URIs present in the model 191 * 192 * @param model model 193 * @param nsPrefixes map of namespace to uris 194 * @return nsPrefixes filtered to namespaces found in the model 195 */ 196 private static Map<String, String> filterNamespacesToPresent(final Model model, 197 final Map<String, String> nsPrefixes) { 198 final Map<String, String> resultNses = new HashMap<>(); 199 final Set<Entry<String, String>> nsSet = nsPrefixes.entrySet(); 200 final NsIterator nsIt = model.listNameSpaces(); 201 while (nsIt.hasNext()) { 202 final String ns = nsIt.next(); 203 204 final Optional<Entry<String, String>> nsOpt = nsSet.stream() 205 .filter(nsEntry -> nsEntry.getValue().equals(ns)) 206 .findFirst(); 207 if (nsOpt.isPresent()) { 208 final Entry<String, String> nsMatch = nsOpt.get(); 209 resultNses.put(nsMatch.getKey(), nsMatch.getValue()); 210 } 211 } 212 213 return resultNses; 214 } 215 216 private static RDFFormat getFormatFromMediaType(final MediaType mediaType) { 217 final String profile = mediaType.getParameters().getOrDefault("profile", ""); 218 if (profile.equals(JSONLD_COMPACTED)) { 219 return JSONLD_COMPACT_FLAT; 220 } else if (profile.equals(JSONLD_FLATTENED)) { 221 return JSONLD_FLATTEN_FLAT; 222 } 223 return JSONLD_EXPAND_FLAT; 224 } 225}