package de.unikiel.inf.comsys.neo4j.http;

/*
 * #%L
 * neo4j-sparql-extension
 * %%
 * Copyright (C) 2014 Niclas Hoyer
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */

import de.unikiel.inf.comsys.neo4j.http.streams.ChunkedCommitHandler;
import de.unikiel.inf.comsys.neo4j.SPARQLExtensionProps;
import de.unikiel.inf.comsys.neo4j.http.streams.RDFStreamingOutput;
import de.unikiel.inf.comsys.neo4j.inference.QueryRewriterFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant;
import org.openrdf.model.Resource;
import org.openrdf.model.ValueFactory;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.sail.SailRepository;
import org.openrdf.repository.sail.SailRepositoryConnection;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFHandlerException;
import org.openrdf.rio.RDFParseException;
import org.openrdf.rio.RDFParser;

/**
 * Implementation of the SPARQL 1.1 Graph Store HTTP Protocol.
 *
 * @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/">
 * SPARQL 1.1 Graph Store HTTP Protocol
 * </a>
 */
public class GraphStore extends AbstractSailResource {

	private final ValueFactory vf;
	private final long chunksize;

	/**
	 * Create a new graph store management resource based on a repository.
	 *
	 * @param rep the repository this resources operates on
	 */
	public GraphStore(SailRepository rep) {
		super(rep);
		this.vf = rep.getValueFactory();
		String chunksizeStr = SPARQLExtensionProps.getProperty("chunksize");
		chunksize = Long.parseLong(chunksizeStr);
	}

	/**
	 * Indirect HTTP GET
	 *
	 * @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-get">
	 * Section 5.2 "HTTP GET"
	 * </a>
	 * @param req JAX-RS {@link Request} object
	 * @param graphString the "graph" query parameter
	 * @param def the "default" query parameter
	 * @return the content of the request graph as HTTP response
	 */
	@GET
	@Produces({
		RDFMediaType.RDF_TURTLE,
		RDFMediaType.RDF_XML,
		RDFMediaType.RDF_NTRIPLES,
		RDFMediaType.RDF_JSON
	})
	public Response graphIndirectGet(
			@Context Request req,
			@QueryParam("graph") String graphString,
			@QueryParam("default") String def) {
		return handleGet(req, def, graphString);
	}

	/**
	 * Indirect HTTP PUT
	 *
	 * @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-put">
	 * Section 5.3 "HTTP PUT"
	 * </a>
	 * @param uriInfo JAX-RS {@link UriInfo} object
	 * @param type Content-Type HTTP header field
	 * @param graphString the "graph" query parameter
	 * @param def the "default" query parameter
	 * @param chunked the "chunked" query parameter
	 * @param in HTTP body as {@link InputStream}
	 * @return "204 No Content", if operation was successful
	 */
	@PUT
	@Consumes({
		RDFMediaType.RDF_TURTLE,
		RDFMediaType.RDF_XML,
		RDFMediaType.RDF_NTRIPLES,
		RDFMediaType.RDF_XML
	})
	public Response graphIndirectPut(
			@Context UriInfo uriInfo,
			@HeaderParam("Content-Type") MediaType type,
			@QueryParam("graph") String graphString,
			@QueryParam("default") String def,
			@QueryParam("chunked") String chunked,
			InputStream in) {
		return handleAdd(uriInfo, type, graphString, def, in, chunked, true);
	}

	/**
	 * Indirect HTTP DELETE
	 *
	 * @see <a
	 * href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-delete">
	 * Section 5.4 "HTTP DELETE"
	 * </a>
	 * @param graphString the "graph" query parameter
	 * @param def the "default" query parameter
	 * @return "204 No Content", if operation was successful
	 */
	@DELETE
	public Response graphIndirectDelete(
			@QueryParam("graph") String graphString,
			@QueryParam("default") String def) {
		return handleClear(graphString);
	}

	/**
	 * Indirect HTTP POST
	 *
	 * @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-post">
	 * Section 5.5 "HTTP POST"
	 * </a>
	 * @param uriInfo JAX-RS {@link UriInfo} object
	 * @param type Content-Type HTTP header field
	 * @param graphString the "graph" query parameter
	 * @param def the "default" query parameter
	 * @param chunked the "chunked" query parameter
	 * @param in HTTP body as {@link InputStream}
	 * @return "204 No Content", if operation was successful
	 */
	@POST
	@Consumes({
		RDFMediaType.RDF_TURTLE,
		RDFMediaType.RDF_XML,
		RDFMediaType.RDF_NTRIPLES,
		RDFMediaType.RDF_XML
	})
	public Response graphIndirectPost(
			@Context UriInfo uriInfo,
			@HeaderParam("Content-Type") MediaType type,
			@QueryParam("graph") String graphString,
			@QueryParam("default") String def,
			@QueryParam("chunked") String chunked,
			InputStream in) {
		return handleAdd(uriInfo, type, graphString, def, in, chunked, false);
	}

	/**
	 * Direct HTTP GET
	 *
	 * @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-get">
	 * Section 5.2 "HTTP GET"
	 * </a>
	 * @param req JAX-RS {@link Request} object
	 * @param uriInfo JAX-RS {@link UriInfo} object
	 * @param graphString the "graph" query parameter
	 * @return the content of the request graph as HTTP response
	 */
	@GET
	@Produces({
		RDFMediaType.RDF_TURTLE,
		RDFMediaType.RDF_XML,
		RDFMediaType.RDF_NTRIPLES,
		RDFMediaType.RDF_JSON
	})
	@Path("/{graph}")
	public Response graphDirectGet(
			@Context Request req,
			@Context UriInfo uriInfo,
			@PathParam("graph") String graphString) {
		String graphuri = uriInfo.getAbsolutePath().toASCIIString();
		return handleGet(req, null, graphuri);
	}

	/**
	 * Direct HTTP PUT
	 *
	 * @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-put">
	 * Section 5.3 "HTTP PUT"
	 * </a>
	 * @param uriInfo JAX-RS {@link UriInfo} object
	 * @param type Content-Type HTTP header field
	 * @param chunked the "chunked" query parameter
	 * @param in HTTP body as {@link InputStream}
	 * @return "204 No Content", if operation was successful
	 */
	@PUT
	@Consumes({
		RDFMediaType.RDF_TURTLE,
		RDFMediaType.RDF_XML,
		RDFMediaType.RDF_NTRIPLES,
		RDFMediaType.RDF_XML
	})
	@Path("/{graph}")
	public Response graphDirectPut(
			@Context UriInfo uriInfo,
			@HeaderParam("Content-Type") MediaType type,
			@QueryParam("chunked") String chunked,
			InputStream in) {
		String graphuri = uriInfo.getAbsolutePath().toASCIIString();
		return handleAdd(uriInfo, type, graphuri, null, in, chunked, true);
	}

	/**
	 * Direct HTTP DELETE
	 *
	 * @see <a
	 * href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-delete">
	 * Section 5.4 "HTTP DELETE"
	 * </a>
	 * @param uriInfo JAX-RS {@link UriInfo} object
	 * @return "204 No Content", if operation was successful
	 */
	@DELETE
	@Path("/{graph}")
	public Response graphDirectDelete(@Context UriInfo uriInfo) {
		String graphuri = uriInfo.getAbsolutePath().toASCIIString();
		return handleClear(graphuri);
	}

	/**
	 * Direct HTTP POST
	 *
	 * @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-post">
	 * Section 5.5 "HTTP POST"
	 * </a>
	 * @param uriInfo JAX-RS {@link UriInfo} object
	 * @param type Content-Type HTTP header field
	 * @param chunked the "chunked" query parameter
	 * @param in HTTP body as {@link InputStream}
	 * @return "204 No Content", if operation was successful
	 */
	@POST
	@Consumes({
		RDFMediaType.RDF_TURTLE,
		RDFMediaType.RDF_XML,
		RDFMediaType.RDF_NTRIPLES,
		RDFMediaType.RDF_XML
	})
	@Path("/{graph}")
	public Response graphDirectPost(
			@Context UriInfo uriInfo,
			@HeaderParam("Content-Type") MediaType type,
			@QueryParam("chunked") String chunked,
			InputStream in) {
		String graphuri = uriInfo.getAbsolutePath().toASCIIString();
		return handleAdd(uriInfo, type, graphuri, null, in, chunked, false);
	}

	/**
	 * Adds RDF data to a graph in the repository.
	 *
	 * @param uriInfo JAX-RS {@link UriInfo} object
	 * @param type Content-Type HTTP header field
	 * @param graphString the "graph" query parameter
	 * @param def the "default" query parameter
	 * @param in RDF data
	 * @param chunkedStr the "chunked" query parameter
	 * @param clear true if the graph should be cleared before adding data
	 * @return "204 No Content", if operation was successful
	 */
	private Response handleAdd(
			UriInfo uriInfo,
			MediaType type,
			String graphString,
			String def,
			InputStream in,
			String chunkedStr,
			boolean clear) {
		SailRepositoryConnection conn;
		try {
			conn = getConnection();
		} catch (RepositoryException ex) {
			throw new WebApplicationException(ex);
		}
		try {
			boolean chunked = chunkedStr != null && chunkedStr.equals("true");
			Resource dctx = null;
			// get the base URI by using direct or indirect graph reference
			String base = uriInfo.getAbsolutePath().toASCIIString();
			if (graphString != null) {
				dctx = vf.createURI(graphString);
				base = dctx.stringValue();
			}
			// check if a Content-Type header was set
			if (type == null) {
				return Response.status(Response.Status.BAD_REQUEST).build();
			}
			String typestr = type.getType() + "/" + type.getSubtype();
			RDFFormat format = getRDFFormat(typestr);
			// begin import
			conn.begin();
			if (dctx != null) {
				if (clear) {
					conn.clear(dctx);
				}
				addToGraphstore(conn, in, base, format, dctx, chunked);
			} else {
				if (clear) {
					conn.clear();
				}
				addToGraphstore(conn, in, base, format, null, chunked);
			}
			conn.commit();
			// check for modifications of TBox-graph and notify the query
			// rewriting component
			if (dctx != null) {
				QueryRewriterFactory qr = QueryRewriterFactory.getInstance(rep);
				if (dctx.stringValue().equals(qr.getOntologyContext())) {
					qr.updateOntology(conn);
				}
			}
			close(conn);
			return Response.noContent().build();
		} catch (RDFParseException ex) {
			// rdf syntax error
			String str = ex.getMessage();
			close(conn, ex);
			return Response.status(400).entity(
					str.getBytes(Charset.forName("UTF-8"))).build();
		} catch (IOException | RepositoryException | RDFHandlerException ex) {
			// server error
			close(conn, ex);
			throw new WebApplicationException(ex);
		}
	}

	/**
	 * Helper method for handleAdd.
	 */
	private void addToGraphstore(
			RepositoryConnection conn,
			InputStream in,
			String base,
			RDFFormat format,
			Resource dctx,
			boolean chunked) throws IOException, RDFParseException,
			RDFHandlerException, RepositoryException {
		if (chunked) {
			RDFParser parser = getRDFParser(format);
			parser.setRDFHandler(
					new ChunkedCommitHandler(conn, chunksize, dctx));
			parser.parse(in, base);
		} else {
			if (dctx != null) {
				conn.add(in, base, format, dctx);
			} else {
				conn.add(in, base, format);
			}
		}
	}

	/**
	 * Deletes all data from a graph in the repository.
	 *
	 * @param graphString the graph to delete
	 * @return "204 No Content", if operation was successful
	 */
	private Response handleClear(String graphString) {
		SailRepositoryConnection conn;
		try {
			conn = getConnection();
		} catch (RepositoryException ex) {
			throw new WebApplicationException(ex);
		}
		try {
			conn.begin();
			if (graphString != null) {
				Resource ctx = vf.createURI(graphString);
				conn.clear(ctx);
				// check if TBox graph has been cleared and notify query
				// rewriting component
				QueryRewriterFactory qr = QueryRewriterFactory.getInstance(rep);
				if (ctx.stringValue().equals(qr.getOntologyContext())) {
					qr.updateOntology(conn);
				}
			} else {
				conn.clear();
			}
			conn.commit();
			close(conn);
		} catch (RepositoryException ex) {
			// server error
			close(conn, ex);
			throw new WebApplicationException(ex);
		}
		return Response.noContent().build();
	}

	/**
	 * Returns RDF data from a graph in the repository.
	 *
	 * @see RDFStreamingOutput
	 * @param req JAX-RS {@link Request} object
	 * @param def the "default" query parameter
	 * @param graphString the "graph" query parameter
	 * @return RDF data as HTTP response
	 */
	private Response handleGet(
			Request req,
			String def,
			String graphString) {
		// select matching MIME-Type for response based on HTTP headers
		final Variant variant = req.selectVariant(rdfResultVariants);
		final MediaType mt = variant.getMediaType();
		final String mtstr = mt.getType() + "/" + mt.getSubtype();
		final RDFFormat format = getRDFFormat(mtstr);
		StreamingOutput stream;
		RepositoryConnection conn = null;
		try {
			// return data as RDF stream
			conn = getConnection();
			if (graphString != null) {
				Resource ctx = vf.createURI(graphString);
				if (conn.size(ctx) == 0) {
					return Response.status(Response.Status.NOT_FOUND).build();
				}
				stream = new RDFStreamingOutput(conn, format, ctx);
			} else {
				stream = new RDFStreamingOutput(conn, format);
			}
		} catch (RepositoryException ex) {
			// server error
			close(conn, ex);
			throw new WebApplicationException(ex);
		}
		return Response.ok(stream).build();
	}

}