org.eclipse.lyo.oslc.v3.sample.BugContainer.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.lyo.oslc.v3.sample.BugContainer.java

Source

/*******************************************************************************
 * Copyright (c) 2015 IBM Corporation.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 *
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *
 *    Samuel Padgett      - initial API and implementation
 *******************************************************************************/
package org.eclipse.lyo.oslc.v3.sample;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;

import org.apache.http.HeaderElement;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicHeaderValueParser;
import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.atlas.json.JSON;
import org.apache.jena.atlas.json.JsonObject;
import org.apache.jena.riot.Lang;
import org.apache.log4j.Logger;
import org.eclipse.lyo.oslc.v3.sample.vocab.LDP;
import org.eclipse.lyo.oslc.v3.sample.vocab.OSLC;
import org.eclipse.lyo.oslc.v3.sample.vocab.OSLC_CM;

import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.vocabulary.DCTerms;

import static javax.ws.rs.core.MediaType.*;
import static org.eclipse.lyo.oslc.v3.sample.Headers.*;
import static org.eclipse.lyo.oslc.v3.sample.MediaTypeContants.*;
import static org.eclipse.lyo.oslc.v3.sample.vocab.LDP.*;
import static org.eclipse.lyo.oslc.v3.sample.vocab.OSLC.*;

@Path("/bugs")
public class BugContainer {
    private static final Logger logger = Logger.getLogger(BugContainer.class);

    @Context
    private HttpServletRequest request;
    @Context
    private HttpServletResponse response;
    @Context
    private UriInfo uriInfo;
    @Context
    private HttpHeaders headers;

    private static final String PREVIEW_WIDTH = "400px";
    private static final String PREVIEW_HEIGHT = "200px";

    private Set<String> include = new HashSet<String>();
    private Set<String> omit = new HashSet<String>();

    private static final Map<String, String> SEVERITY_LABELS = new HashMap<String, String>();
    static {
        SEVERITY_LABELS.put(OSLC_CM.NS + "Blocker", "Blocker");
        SEVERITY_LABELS.put(OSLC_CM.NS + "Critical", "Critical");
        SEVERITY_LABELS.put(OSLC_CM.NS + "Major", "Major");
        SEVERITY_LABELS.put(OSLC_CM.NS + "Normal", "Normal");
        SEVERITY_LABELS.put(OSLC_CM.NS + "Minor", "Minor");
        SEVERITY_LABELS.put(OSLC_CM.NS + "SeverityUnassigned", "Unassigned");
    }

    private static final JsonObject COMPACT_CONTEXT;
    static {
        final InputStream in = BugContainer.class.getResourceAsStream("/preview.jsonld");
        COMPACT_CONTEXT = JSON.parse(in);
    }

    @GET
    @Produces(TEXT_TURTLE)
    public StreamingOutput getContainerTurtle() {
        return getContainer(Lang.TURTLE);
    }

    @GET
    @Produces({ APPLICATION_JSON_LD, APPLICATION_JSON })
    public StreamingOutput getContainerJSONLD() {
        return getContainer(Lang.JSONLD);
    }

    public StreamingOutput getContainer(final Lang lang) {
        setContainerResponseHeaders();
        final String requestURI = getRequestURI();
        return new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {
                Persistence.getInstance().readLock();
                try {
                    Model model = ModelFactory.createDefaultModel();
                    Resource container = model.createResource(requestURI, LDP.BasicContainer);
                    container.addProperty(DCTerms.title, "Bug Container");

                    // Check the Prefer header to see what to include or omit.
                    parsePrefer();

                    // Include dialogs?
                    if (include.contains(OSLC.NS + "PreferDialog")) {
                        setPreferenceAppliedHeader();
                        createDialogResource(model);
                    }

                    // Include containment by default. This is up to the server.
                    boolean includeContainment = true;

                    // Include containment?
                    if (include.contains(LDP.NS + "PreferContainment")) {
                        setPreferenceAppliedHeader();
                    } else if (include.contains(LDP.NS + "PreferMinimalContainer")
                            || omit.contains(LDP.NS + "PreferContainment")) {
                        setPreferenceAppliedHeader();
                        includeContainment = false;
                    }

                    if (includeContainment) {
                        Persistence.getInstance().addContainmentTriples(container);
                    }

                    respond(out, model, lang.getName(), requestURI);
                } finally {
                    Persistence.getInstance().end();
                }
            }
        };
    }

    @OPTIONS
    public void bugContainerOptions() {
        setContainerResponseHeaders(false);
    }

    @GET
    @Path("creationDialog")
    @Produces({ TEXT_TURTLE, APPLICATION_JSON_LD, APPLICATION_JSON })
    public Model getCreationDialogDescriptor() {
        return createDialogModel();
    }

    @OPTIONS
    @Path("creationDialog")
    public void creationDialogDescriptorOptions() {
        // Prefill is not yet supported.
        response.addHeader(ALLOW, "GET,HEAD,OPTIONS");
    }

    @GET
    @Path("{id}")
    @Produces(TEXT_TURTLE)
    public StreamingOutput getBugTurtle() {
        return getBug(Lang.TURTLE);
    }

    @GET
    @Path("{id}")
    @Produces(APPLICATION_JSON_LD)
    public StreamingOutput getBugJSONLD() {
        return getBug(Lang.JSONLD);
    }

    public StreamingOutput getBug(final Lang lang) {
        final String bugURI = getRequestURI();
        return new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {
                Persistence.getInstance().readLock();
                try {
                    final Model bugModel = getBugModel(bugURI);
                    setBugResponseHeaders();

                    // Check the Prefer header to see what to include or omit.
                    parsePrefer();
                    if (include.contains(OSLC.NS + "PreferCompact")) {
                        // Compact requested.
                        setPreferenceAppliedHeader();
                        final Resource bug = bugModel.getResource(bugURI);
                        Model compactModel = createCompactModel(getBugLabel(bug), bugURI);
                        // Add in the Bug triples (not required, but we have them).
                        compactModel.add(bugModel);
                        respond(out, compactModel, lang.getName(), bugURI);
                    } else {
                        respond(out, bugModel, lang.getName(), bugURI);
                    }
                } finally {
                    Persistence.getInstance().end();
                }
            }
        };
    }

    /*
     * Handle JSON specially so we can return the correct Compact JSON response if requested.
     */
    @GET
    @Path("{id}")
    @Produces({ APPLICATION_JSON })
    public StreamingOutput getBugJSON() {
        final String bugURI = getRequestURI();
        return new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {
                Persistence.getInstance().readLock();
                try {
                    final Model bugModel = getBugModel(bugURI);
                    setBugResponseHeaders();

                    parsePrefer();
                    if (include.contains(OSLC.NS + "PreferCompact")) {
                        // Compact requested. Return the JSON Compact representation.
                        setPreferenceAppliedHeader();

                        final JsonObject jsonResponse = new JsonObject();
                        final JsonObject compact = createCompactJSON(bugURI, bugModel);
                        jsonResponse.put("compact", compact);
                        jsonResponse.put("@context", COMPACT_CONTEXT);

                        respond(out, jsonResponse);
                    } else {
                        // Return the Bug as JSON-LD.
                        respond(out, bugModel, Lang.JSONLD.getName(), bugURI);
                    }
                } finally {
                    Persistence.getInstance().end();
                }
            }
        };
    }

    @GET
    @Path("{id}")
    @Produces(TEXT_HTML)
    public void getBugHTML() throws ServletException, IOException {
        final String bugURI = getRequestURI();
        forwardBugJSP(bugURI, "/WEB-INF/bug.jsp");
    }

    private JsonObject createCompactJSON(final String bugURI, final Model bugModel) {
        final JsonObject compact = new JsonObject();
        final String uri = UriBuilder.fromUri(bugURI).path("compact").build().toString();
        compact.put("@id", uri);

        final Resource bug = bugModel.getResource(bugURI);
        compact.put("title", getBugLabel(bug));
        compact.put("icon", getIconURI().toString());

        final JsonObject preview = new JsonObject();
        final String document = UriBuilder.fromUri(bugURI).path("preview").build().toString();
        preview.put("document", document);
        preview.put("hintWidth", PREVIEW_WIDTH);
        preview.put("hintHeight", PREVIEW_HEIGHT);
        compact.put("smallPreview", preview);

        return compact;
    }

    @GET
    @Path("{id}/compact")
    @Produces({ TEXT_TURTLE, APPLICATION_JSON_LD })
    public Model getCompactRDF(@PathParam("id") String id) {
        final String bugURI = getBugURI(id);
        Persistence.getInstance().readLock();
        try {
            final Model bugModel = getBugModel(bugURI);
            final Resource bug = bugModel.getResource(bugURI);
            final String label = getBugLabel(bug);

            return createCompactModel(label, bugURI);
        } finally {
            Persistence.getInstance().end();
        }
    }

    @GET
    @Path("{id}/compact")
    @Produces({ APPLICATION_JSON })
    public StreamingOutput getCompactJSON(@PathParam("id") final String id) {
        return new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {
                final JsonObject compact;
                final String bugURI = getBugURI(id);
                Persistence.getInstance().readLock();
                try {
                    final Model bugModel = getBugModel(bugURI);
                    compact = createCompactJSON(bugURI, bugModel);
                    compact.put("@context", COMPACT_CONTEXT);
                } finally {
                    Persistence.getInstance().end();
                }

                compact.output(new IndentedWriter(out));
            }
        };
    }

    @GET
    @Path("{id}/preview")
    @Produces(TEXT_HTML)
    public void getBugPreview(@PathParam("id") String id) throws ServletException, IOException {
        final String bugURI = getBugURI(id);
        forwardBugJSP(bugURI, "/WEB-INF/preview.jsp");
    }

    public void forwardBugJSP(final String bugURI, final String path) throws ServletException, IOException {
        Persistence.getInstance().readLock();
        try {
            final Model model = getBugModel(bugURI);
            setBugResponseHeaders();
            setBugAttributes(model, bugURI);
        } finally {
            Persistence.getInstance().end();
        }
        request.getRequestDispatcher(path).forward(request, response);
    }

    private void setBugAttributes(Model model, String bugURI) throws ServletException, IOException {
        request.setAttribute("baseURI", getBaseURI());
        request.setAttribute("bugURI", bugURI);
        final Resource r = model.getResource(bugURI);

        final Statement id = r.getProperty(DCTerms.identifier);
        if (id != null && id.getObject().isLiteral()) {
            request.setAttribute("id", id.getString());
        }

        final Statement title = r.getProperty(DCTerms.title);
        if (title != null && title.getObject().isLiteral()) {
            request.setAttribute("title", title.getString());
        }

        final Statement severity = r.getProperty(OSLC_CM.severity);
        if (severity != null && severity.getObject().isURIResource()) {
            String uri = severity.getResource().getURI();
            request.setAttribute("severity", SEVERITY_LABELS.get(uri));
        }

        final Statement description = r.getProperty(DCTerms.description);
        if (description != null && description.getObject().isLiteral()) {
            request.setAttribute("description", description.getString());
        }

        request.setAttribute("created", getDate(r.getProperty(DCTerms.created)));
    }

    private Date getDate(Statement s) {
        if (s == null || !s.getObject().isLiteral()) {
            return null;
        }

        try {
            return DatatypeFactory.newInstance().newXMLGregorianCalendar(s.getString()).toGregorianCalendar()
                    .getTime();
        } catch (IllegalArgumentException e) {
            logger.warn(String.format("Invalid date format <%s>" + s.getString()), e);
            return null;
        } catch (DatatypeConfigurationException e) {
            logger.error("Error initializing datatype factory", e);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }
    }

    @DELETE
    @Path("{id}")
    public Response deleteBug() {
        Persistence.getInstance().writeLock();
        try {
            verifyBugExists();
            Persistence.getInstance().removeBugModel(getRequestURI());
            Persistence.getInstance().commit();
        } finally {
            Persistence.getInstance().end();
        }

        return Response.noContent().build();
    }

    @OPTIONS
    @Path("{id}")
    public void bugOptions() {
        Persistence.getInstance().readLock();
        try {
            verifyBugExists();
        } finally {
            Persistence.getInstance().end();
        }

        setBugResponseHeaders(false);
    }

    @POST
    @Consumes(TEXT_TURTLE)
    public Response createBugTurtle(InputStream in) {
        return createBug(in, Lang.TURTLE);
    }

    @POST
    @Consumes({ APPLICATION_JSON_LD, APPLICATION_JSON })
    public Response createBugJSON(InputStream in) {
        return createBug(in, Lang.JSONLD);
    }

    private Response createBug(InputStream in, Lang lang) {
        setContainerResponseHeaders();

        Persistence.getInstance().writeLock();
        try {
            // Create a URI.
            final String id = Persistence.getInstance().reserveID().toString();
            final URI location = getRequestURIBuilder().path(id).build();

            // Read the model ourselves so we can set the correct base to resolve relative URIs.
            final Model m = ModelFactory.createDefaultModel();
            try {
                m.read(in, location.toString(), lang.getName());

                // Add a dcterms:created triple
                Resource r = m.createResource(location.toString());
                if (!r.hasProperty(DCTerms.created)) {
                    r.addProperty(DCTerms.created, m.createTypedLiteral(Calendar.getInstance()));
                }

                m.removeAll(r, DCTerms.identifier, null);
                r.addProperty(DCTerms.identifier, id);
            } catch (Exception e) {
                return Response.status(Status.BAD_REQUEST).build();
            }

            Persistence.getInstance().addBugModel(m, location);
            Persistence.getInstance().commit();

            return Response.created(location).build();
        } finally {
            Persistence.getInstance().end();
        }
    }

    @GET
    @Path("sparql")
    @Produces(TEXT_HTML)
    public void showSPARQLPage() throws ServletException, IOException {
        request.setAttribute("baseURI", getBaseURI());
        request.setAttribute("endpoint", getRequestURI());
        request.getRequestDispatcher("/WEB-INF/sparql.jsp").forward(request, response);
    }

    @POST
    @Path("sparql")
    @Consumes(APPLICATION_SPARQL_QUERY)
    @Produces(APPLICATION_SPARQL_RESULTS_JSON)
    public StreamingOutput postQuery(final String queryString) {
        return new StreamingOutput() {
            @Override
            public void write(OutputStream out) throws IOException, WebApplicationException {
                Persistence.getInstance().readLock();
                try {
                    Persistence.getInstance().query(queryString, out);
                } catch (Exception e) {
                    throw new WebApplicationException(Response.status(Status.BAD_REQUEST).build());
                } finally {
                    Persistence.getInstance().end();
                }
            }
        };
    }

    private void respond(final OutputStream out, final Model m, final String lang, final String base)
            throws IOException {
        String etag = ETag.generate(m, lang, base);
        testIfNoneMatch(etag);
        setETagHeader(etag);
        m.write(out, lang, base);
    }

    private void respond(final OutputStream out, final JsonObject json) throws IOException {
        String etag = ETag.generate(json);
        testIfNoneMatch(etag);
        setETagHeader(etag);
        json.output(new IndentedWriter(out));
    }

    private String getBaseURI() {
        return getBaseUriBuilder().path("..").build().normalize().toString().replaceAll("/$", "");
    }

    private URI getStaticResource(String path) {
        return getBaseUriBuilder().path("..").path(path).build().normalize();
    }

    private String getBugURI(String id) {
        return getBaseUriBuilder().path("bugs/{id}").build(id).toString();
    }

    private URI getDialogURI() {
        return getBaseUriBuilder().path("bugs/creationDialog").build();
    }

    private URI getIconURI() {
        return getStaticResource("oslc-16x16.png");
    }

    private UriBuilder getRequestURIBuilder() {
        // uriInfo.getRequestUriBuilder() incorrectly adds the default port in
        // some environments (e.g., Bluemix)
        return UriBuilder.fromUri(getRequestURI());
    }

    private UriBuilder getBaseUriBuilder() {
        // uriInfo.getBaseUriBuilder() incorrectly adds the default port in
        // some environments (e.g., Bluemix)
        try {
            final String basePath = uriInfo.getBaseUri().getRawPath();
            final URI baseURI = new URI(getRequestURI()).resolve(basePath);

            return UriBuilder.fromUri(baseURI);
        } catch (URISyntaxException e) {
            // Should never happen.
            logger.error("Error determining base URI", e);
            throw new RuntimeException(e);
        }
    }

    private String getRequestURI() {
        return request.getRequestURL().toString();
    }

    private String getBugLabel(final Resource bug) {
        final Statement title = bug.getProperty(DCTerms.title);
        final Statement id = bug.getProperty(DCTerms.identifier);
        if (!literal(title)) {
            if (!literal(id)) {
                return "<Untitled>";
            }

            return id.getString();
        }

        if (!literal(id)) {
            return title.getString();
        }

        return id.getString() + ": " + title.getString();
    }

    private boolean literal(Statement s) {
        return s != null && s.getObject().isLiteral();
    }

    private Model createCompactModel(String title, String bugURI) {
        Model m = ModelFactory.createDefaultModel();
        String compactURI = UriBuilder.fromUri(bugURI).path("compact").build().toString();
        Resource compact = m.createResource(compactURI, OSLC.Compact);
        compact.addProperty(DCTerms.title, title);
        compact.addProperty(OSLC.icon, m.createResource(getIconURI().toString()));

        Resource preview = m.createResource(OSLC.Preview);
        String document = UriBuilder.fromUri(bugURI).path("preview").build().toString();
        preview.addProperty(OSLC.document, m.createResource(document));
        preview.addProperty(OSLC.hintWidth, PREVIEW_WIDTH);
        preview.addProperty(OSLC.hintHeight, PREVIEW_HEIGHT);

        compact.addProperty(OSLC.smallPreview, preview);

        return m;
    }

    private Model createDialogModel() {
        Model m = ModelFactory.createDefaultModel();
        createDialogResource(m);

        return m;
    }

    private void createDialogResource(Model m) {
        Resource dialog = m.createResource(getDialogURI().toString(), OSLC.Dialog);
        dialog.addProperty(DCTerms.title, "Open Bug");
        dialog.addProperty(OSLC.label, "Open Bug");
        String document = getStaticResource("creationDialog.html").toString();
        dialog.addProperty(OSLC.dialog, m.createResource(document));
        dialog.addProperty(OSLC.hintWidth, "450px");
        dialog.addProperty(OSLC.hintHeight, "395px");
    }

    private Model getBugModel(String uri) {
        final Model model = Persistence.getInstance().getBugModel(uri);
        if (model == null) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }

        return model;
    }

    private void setLinkHeader(URI uri, String relation) {
        setLinkHeader(uri.toString(), relation);
    }

    private void setLinkHeader(String uri, String relation) {
        response.addHeader(LINK, "<" + uri + ">; rel=\"" + relation + "\"");
    }

    private void setBugResponseHeaders() {
        setBugResponseHeaders(true);
    }

    private void setBugResponseHeaders(boolean includeCacheHeaders) {
        response.addHeader(ALLOW, "GET,HEAD,OPTIONS,DELETE");
        if (includeCacheHeaders) {
            response.addHeader(CACHE_CONTROL, "no-cache");
            response.addHeader(VARY, "Accept,Prefer");
        }
        setLinkHeader(LDP.Resource.getURI(), LINK_REL_TYPE);
        setLinkHeader(getRequestURIBuilder().path("compact").build(), LINK_REL_COMPACT);
    }

    private void setContainerResponseHeaders() {
        setContainerResponseHeaders(true);
    }

    private void setContainerResponseHeaders(boolean includeCacheHeaders) {
        // LDP Headers
        response.addHeader(ALLOW, "GET,HEAD,POST,OPTIONS");
        response.addHeader(ACCEPT_POST, TEXT_TURTLE + "," + APPLICATION_JSON + "," + APPLICATION_JSON);
        if (includeCacheHeaders) {
            response.addHeader(CACHE_CONTROL, "no-cache");
            response.addHeader(VARY, "Accept,Prefer");
        }
        setLinkHeader(LDP.Resource.getURI(), LINK_REL_TYPE);
        setLinkHeader(LDP.BasicContainer.getURI(), LINK_REL_TYPE);
        setLinkHeader(OSLC_CM.Defect.getURI(), OSLC.creationType.getURI());

        // LDP constrainedBy header should point to the resource shape
        URI shape = uriInfo.getBaseUriBuilder().path("../Defect-shape.ttl").build().normalize();
        setLinkHeader(shape, LINK_REL_CONSTRAINED_BY);

        // OSLC Creation Dialog
        setLinkHeader(getDialogURI(), LINK_REL_CREATION_DIALOG);
    }

    private void setETagHeader(String etag) {
        response.addHeader(ETAG, etag);
    }

    private void verifyBugExists() {
        if (!Persistence.getInstance().exists(getRequestURI())) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
    }

    private void parsePrefer() {
        final List<String> preferValues = headers.getRequestHeader(PREFER);
        if (preferValues == null) {
            return;
        }

        for (String prefer : preferValues) {
            HeaderElement[] preferElements = BasicHeaderValueParser.parseElements(prefer, null);
            for (HeaderElement e : preferElements) {
                if ("return".equals(e.getName()) && "representation".equals(e.getValue())) {
                    addValues(e.getParameterByName("include"), include);
                    addValues(e.getParameterByName("omit"), omit);
                }
            }
        }
    }

    private void addValues(NameValuePair parameter, Set<String> values) {
        if (parameter != null) {
            String parameterValue = parameter.getValue();
            if (parameterValue != null) {
                for (String s : parameterValue.split(" ")) {
                    values.add(s);
                }
            }
        }
    }

    private void testIfNoneMatch(String etag) {
        final List<String> ifNoneMatchValues = headers.getRequestHeader(IF_NONE_MATCH);
        if (ifNoneMatchValues == null) {
            return;
        }

        for (String ifNoneMatch : ifNoneMatchValues) {
            if (ETag.matches(ifNoneMatch, etag)) {
                throw new WebApplicationException(Status.NOT_MODIFIED);
            }
        }
    }

    /**
     * Sets the Preference-Applied response header for preference <code>return=representation</code>.
     */
    private void setPreferenceAppliedHeader() {
        response.setHeader(PREFERENCE_APPLIED, "return=representation");
    }
}