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