001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.http.commons.responses;
017
018import static com.google.common.collect.Maps.filterEntries;
019import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE;
020import static org.fcrepo.kernel.api.utils.UncheckedBiConsumer.uncheck;
021import static org.openrdf.model.impl.ValueFactoryImpl.getInstance;
022import static org.openrdf.model.util.Literals.createLiteral;
023import static org.slf4j.LoggerFactory.getLogger;
024
025import java.io.OutputStream;
026import java.util.function.Function;
027
028import javax.ws.rs.WebApplicationException;
029import javax.ws.rs.core.MediaType;
030import javax.ws.rs.core.StreamingOutput;
031
032import org.fcrepo.kernel.api.utils.iterators.RdfStream;
033
034import org.openrdf.model.Resource;
035import org.openrdf.model.Statement;
036import org.openrdf.model.URI;
037import org.openrdf.model.Value;
038import org.openrdf.model.ValueFactory;
039import org.openrdf.rio.RDFFormat;
040import org.openrdf.rio.RDFHandlerException;
041import org.openrdf.rio.RDFWriter;
042import org.openrdf.rio.RDFWriterRegistry;
043import org.openrdf.rio.Rio;
044import org.openrdf.rio.WriterConfig;
045import org.slf4j.Logger;
046
047import com.google.common.util.concurrent.AbstractFuture;
048import com.hp.hpl.jena.graph.Node;
049import com.hp.hpl.jena.graph.Triple;
050
051/**
052 * Serializes an {@link RdfStream}.
053 *
054 * @author ajs6f
055 * @since Oct 30, 2013
056 */
057public class RdfStreamStreamingOutput extends AbstractFuture<Void> implements
058        StreamingOutput {
059
060    private static final Logger LOGGER =
061        getLogger(RdfStreamStreamingOutput.class);
062
063    private static ValueFactory vfactory = getInstance();
064
065    /**
066     * This field is used to determine the correct {@link org.openrdf.rio.RDFWriter} created for the
067     * {@link javax.ws.rs.core.StreamingOutput}.
068     */
069    private final RDFFormat format;
070
071    /**
072     * This field is used to determine the {@link org.openrdf.rio.WriterConfig} details used by the created
073     * {@link org.openrdf.rio.RDFWriter}.
074     */
075    private final MediaType mediaType;
076
077    private final RdfStream rdfStream;
078
079    /**
080     * Normal constructor
081     *
082     * @param rdfStream the rdf stream
083     * @param mediaType the media type
084     */
085    public RdfStreamStreamingOutput(final RdfStream rdfStream,
086            final MediaType mediaType) {
087        super();
088
089        if (LOGGER.isDebugEnabled()) {
090            for (final RDFFormat writeableFormats : RDFWriterRegistry.getInstance().getKeys()) {
091                LOGGER.debug("Discovered RDF writer writeableFormats: {} with mimeTypes: {}",
092                        writeableFormats.getName(), String.join(" ", writeableFormats.getMIMETypes()));
093            }
094        }
095        final RDFFormat format = Rio.getWriterFormatForMIMEType(mediaType.toString());
096        if (format != null) {
097            this.format = format;
098            this.mediaType = mediaType;
099            LOGGER.debug("Setting up to serialize to: {}", format);
100        } else {
101            throw new WebApplicationException(NOT_ACCEPTABLE);
102        }
103
104        this.rdfStream = rdfStream;
105    }
106
107    @Override
108    public void write(final OutputStream output) {
109        LOGGER.debug("Serializing RDF stream in: {}", format);
110        try {
111            write(() -> rdfStream.transform(toStatement::apply), output, format, mediaType);
112        } catch (final RDFHandlerException e) {
113            setException(e);
114            LOGGER.debug("Error serializing RDF", e);
115            throw new WebApplicationException(e);
116        }
117    }
118
119    private void write(final Iterable<Statement> model,
120                       final OutputStream output,
121                       final RDFFormat dataFormat,
122                       final MediaType dataMediaType)
123            throws RDFHandlerException {
124        final WriterConfig settings = WriterConfigHelper.apply(dataMediaType);
125        final RDFWriter writer = Rio.createWriter(dataFormat, output);
126        writer.setWriterConfig(settings);
127
128        /**
129         * We exclude:
130         *  - xmlns, which Sesame helpfully serializes, but normal parsers may complain
131         *     about in some serializations (e.g. RDF/XML where xmlns:xmlns is forbidden by XML);
132         */
133        filterEntries(rdfStream.namespaces(), e -> !e.getKey().equals("xmlns"))
134                .forEach(uncheck(writer::handleNamespace));
135
136        Rio.write(model, writer);
137    }
138
139    protected static final Function<? super Triple, Statement> toStatement = t -> {
140        final Resource subject = getResourceForSubject(t.getSubject());
141        final URI predicate = vfactory.createURI(t.getPredicate().getURI());
142        final Value object = getValueForObject(t.getObject());
143        return vfactory.createStatement(subject, predicate, object);
144    };
145
146    private static Resource getResourceForSubject(final Node subjectNode) {
147        return subjectNode.isBlank() ? vfactory.createBNode(subjectNode.getBlankNodeLabel())
148                : vfactory.createURI(subjectNode.getURI());
149    }
150
151    protected static Value getValueForObject(final Node object) {
152        if (object.isURI()) {
153            return vfactory.createURI(object.getURI());
154        } else if (object.isBlank()) {
155            return vfactory.createBNode(object.getBlankNodeLabel());
156        } else if (object.isLiteral()) {
157            final String literalValue = object.getLiteralLexicalForm();
158
159            final String literalDatatypeURI = object.getLiteralDatatypeURI();
160
161            if (!object.getLiteralLanguage().isEmpty()) {
162                return vfactory.createLiteral(literalValue, object.getLiteralLanguage());
163            } else if (literalDatatypeURI != null) {
164                final URI uri = vfactory.createURI(literalDatatypeURI);
165                return vfactory.createLiteral(literalValue, uri);
166            } else {
167                return createLiteral(vfactory, object.getLiteralValue());
168            }
169        }
170        throw new AssertionError("Unable to convert " + object +
171                " to a value, it is neither URI, blank, nor literal!");
172    }
173}