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}