/* Copyright (c) 2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Gabriel Roldan (Boundless) - initial implementation */ package org.locationtech.geogig.rest.repository; import static org.locationtech.geogig.rest.Variants.CSV; import static org.locationtech.geogig.rest.Variants.CSV_MEDIA_TYPE; import static org.locationtech.geogig.rest.Variants.JSON; import static org.locationtech.geogig.rest.Variants.XML; import static org.locationtech.geogig.rest.Variants.getVariantByExtension; import static org.locationtech.geogig.rest.repository.RESTUtils.getGeogig; import java.io.IOException; import java.io.Writer; import java.util.List; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.codehaus.jettison.mapped.MappedNamespaceConvention; import org.codehaus.jettison.mapped.MappedXMLStreamWriter; import org.locationtech.geogig.api.GeoGIG; import org.locationtech.geogig.rest.RestletException; import org.locationtech.geogig.rest.WriterRepresentation; import org.locationtech.geogig.web.api.CommandBuilder; import org.locationtech.geogig.web.api.CommandContext; import org.locationtech.geogig.web.api.CommandResponse; import org.locationtech.geogig.web.api.CommandSpecException; import org.locationtech.geogig.web.api.ParameterSet; import org.locationtech.geogig.web.api.ResponseWriter; import org.locationtech.geogig.web.api.StreamResponse; import org.locationtech.geogig.web.api.WebAPICommand; import org.restlet.Context; import org.restlet.data.Form; import org.restlet.data.MediaType; import org.restlet.data.Request; import org.restlet.data.Response; import org.restlet.resource.Representation; import org.restlet.resource.Resource; import org.restlet.resource.Variant; import com.google.common.base.Optional; import com.google.common.base.Preconditions; /** * */ public class CommandResource extends Resource { @Override public void init(Context context, Request request, Response response) { super.init(context, request, response); List<Variant> variants = getVariants(); variants.add(XML); variants.add(JSON); variants.add(CSV); } @Override public Variant getPreferredVariant() { return getVariantByExtension(getRequest(), getVariants()).or(super.getPreferredVariant()); } @Override public Representation getRepresentation(Variant variant) { Request request = getRequest(); Representation representation = runCommand(variant, request); return representation; } private Representation runCommand(Variant variant, Request request) { final Optional<GeoGIG> geogig = getGeogig(request); Preconditions.checkState(geogig.isPresent()); Representation rep = null; WebAPICommand command = null; Form options = getRequest().getResourceRef().getQueryAsForm(); String commandName = (String) getRequest().getAttributes().get("command"); MediaType format = resolveFormat(options, variant); try { ParameterSet params = new FormParams(options); command = CommandBuilder.build(commandName, params); assert command != null; } catch (CommandSpecException ex) { rep = formatException(ex, format); } try { if (command != null) { RestletContext ctx = new RestletContext(geogig.get()); command.run(ctx); rep = ctx.getRepresentation(format, getJSONPCallback()); } } catch (IllegalArgumentException ex) { rep = formatException(ex, format); } catch (Exception ex) { rep = formatUnexpectedException(ex, format); } return rep; } private Representation formatException(IllegalArgumentException ex, MediaType format) { Logger logger = getLogger(); if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "CommandSpecException", ex); } if (format == CSV_MEDIA_TYPE) { return new StreamWriterRepresentation(format, StreamResponse.error(ex.getMessage())); } return new JettisonRepresentation(format, CommandResponse.error(ex.getMessage()), getJSONPCallback()); } private Representation formatUnexpectedException(Exception ex, MediaType format) { Logger logger = getLogger(); UUID uuid = UUID.randomUUID(); String stack = ""; StackTraceElement[] trace = ex.getStackTrace(); for (int index = 0; index < 5; index++) { if (index < trace.length) { stack += trace[index].toString() + '\t'; } else { break; } } logger.log(Level.SEVERE, "Unexpected exception : " + uuid, ex); if (format == CSV_MEDIA_TYPE) { return new StreamWriterRepresentation(format, StreamResponse.error(stack)); } return new JettisonRepresentation(format, CommandResponse.error(stack), getJSONPCallback()); } private String getJSONPCallback() { Form form = getRequest().getResourceRef().getQueryAsForm(); return form.getFirstValue("callback", null); } private MediaType resolveFormat(Form options, Variant variant) { MediaType retval = variant.getMediaType(); String requested = options.getFirstValue("output_format"); if (requested != null) { if (requested.equalsIgnoreCase("xml")) { retval = MediaType.APPLICATION_XML; } else if (requested.equalsIgnoreCase("json")) { retval = MediaType.APPLICATION_JSON; } else if (requested.equalsIgnoreCase("csv")) { retval = CSV_MEDIA_TYPE; } else { throw new RestletException("Invalid output_format '" + requested + "'", org.restlet.data.Status.CLIENT_ERROR_BAD_REQUEST); } } return retval; } static class RestletContext implements CommandContext { CommandResponse responseContent = null; StreamResponse streamContent = null; final GeoGIG geogig; RestletContext(GeoGIG geogig) { this.geogig = geogig; } @Override public GeoGIG getGeoGIG() { return geogig; } Representation getRepresentation(MediaType format, String callback) { if (streamContent != null) { if (format != CSV_MEDIA_TYPE) { throw new CommandSpecException( "Unsupported Media Type: This response is only compatible with text/csv."); } return new StreamWriterRepresentation(format, streamContent); } if (format != MediaType.APPLICATION_JSON && format != MediaType.APPLICATION_XML) { throw new CommandSpecException( "Unsupported Media Type: This response is only compatible with application/json and application/xml."); } return new JettisonRepresentation(format, responseContent, callback); } @Override public void setResponseContent(CommandResponse responseContent) { this.responseContent = responseContent; } @Override public void setResponseContent(StreamResponse responseContent) { this.streamContent = responseContent; } } public static class JettisonRepresentation extends WriterRepresentation { final CommandResponse impl; String callback; public JettisonRepresentation(MediaType mediaType, CommandResponse impl, String callback) { super(mediaType); this.impl = impl; this.callback = callback; } private XMLStreamWriter createWriter(Writer writer) { final MediaType mediaType = getMediaType(); XMLStreamWriter xml; if (mediaType.getSubType().equalsIgnoreCase("xml")) { try { xml = XMLOutputFactory.newFactory().createXMLStreamWriter(writer); } catch (XMLStreamException ex) { throw new RuntimeException(ex); } callback = null; // this doesn't make sense } else if (mediaType == MediaType.APPLICATION_JSON) { xml = new MappedXMLStreamWriter(new MappedNamespaceConvention(), writer); } else { throw new RuntimeException("mediatype not handled " + mediaType); } return xml; } @Override public void write(Writer writer) throws IOException { XMLStreamWriter stax = null; if (callback != null) { writer.write(callback); writer.write('('); } try { stax = createWriter(writer); impl.write(new ResponseWriter(stax)); stax.flush(); stax.close(); } catch (Exception ex) { throw new IOException(ex); } if (callback != null) { writer.write(");"); } } } static class StreamWriterRepresentation extends WriterRepresentation { final StreamResponse impl; public StreamWriterRepresentation(MediaType mediaType, StreamResponse impl) { super(mediaType); this.impl = impl; } @Override public void write(Writer writer) throws IOException { try { impl.write(writer); } catch (Exception e) { throw new IOException(e); } } } }