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}