Java tutorial
/* * This file is part of Hootenanny. * * Hootenanny 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/>. * * -------------------------------------------------------------------- * * The following copyright notices are generated automatically. If you * have a new notice to add, please use the format: * " * @copyright Copyright ..." * This will properly maintain the copyright information. DigitalGlobe * copyrights will be updated automatically. * * @copyright Copyright (C) 2013, 2014, 2015 DigitalGlobe (http://www.digitalglobe.com/) */ package hoot.services.controllers.osm; import java.sql.Connection; import hoot.services.db.DbUtils; import hoot.services.db2.QMaps; import hoot.services.models.osm.Changeset; import hoot.services.models.osm.ModelDaoUtils; import hoot.services.utils.ResourceErrorHandler; import hoot.services.utils.XmlDocumentBuilder; import hoot.services.writers.osm.ChangesetDbWriter; import javax.ws.rs.Consumes; import javax.ws.rs.OPTIONS; 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.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.xml.transform.dom.DOMSource; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.w3c.dom.Document; import com.mysema.query.sql.SQLQuery; /** * Service endpoint for an OSM changeset */ @Path("/api/0.6/changeset") public class ChangesetResource { private static final Logger log = LoggerFactory.getLogger(ChangesetResource.class); private ClassPathXmlApplicationContext appContext; private PlatformTransactionManager transactionManager; public ChangesetResource() { log.debug("Reading application settings..."); appContext = new ClassPathXmlApplicationContext(new String[] { "db/spring-database.xml" }); log.debug("Initializing transaction manager..."); transactionManager = appContext.getBean("transactionManager", PlatformTransactionManager.class); } /** * Service method endpoint for creating a pre-flight request for a new OSM changeset; required for * CORS (http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support * * @return Empty response with CORS headers */ @OPTIONS @Path("/create") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_PLAIN) public Response createPreflight() { log.debug("Handling changeset create pre-flight request..."); return Response.ok().build(); } /** * <NAME>Changeset Create</NAME> * <DESCRIPTION> * The Hootenanny Changeset Service implements a subset of the OSM * Changeset Service v0.6. It supports the OSM changeset upload process only. * It does not support the browsing of changeset contents. * </DESCRIPTION> * <PARAMETERS> * <mapId> * string; ID or name of the map to which the created changeset will belong * </mapId> * <changesetData> * XML; payload data; an empty OSM xml changeset * </changesetData> * </PARAMETERS> * <OUTPUT> * ID of the created changeset * </OUTPUT> * <EXAMPLE> * <URL>http://localhost:8080/hoot-services/osm/api/0.6/changeset/create?mapId=dc-admin</URL> * <REQUEST_TYPE>PUT</REQUEST_TYPE> * <INPUT> * Changeset OSM XML * See https://insightcloud.digitalglobe.com/redmine/projects/hootenany/wiki/User_-_OsmChangesetService#Changeset-Create * </INPUT> * <OUTPUT> * 1 * </OUTPUT> * </EXAMPLE> * * Service method endpoint for creating a new OSM changeset * * @param changeset changeset create data * @param mapId ID of the map the changeset belongs to * @return Response containing the ID assigned to the new changeset * @throws Exception * @todo why can't I get changesetData in as an XML doc? * @todo update for parsing multiple changesets in one request (#2894): duplicated changeset tag keys are allowed but later changeset tag keys overwrite earlier ones; isn't that contradictory with the rest of the logic in this method? * @see https://insightcloud.digitalglobe.com/redmine/projects/hootenany/wiki/User_-_OsmChangesetService#Changeset-Create */ @PUT @Path("/create") @Consumes(MediaType.TEXT_XML) @Produces(MediaType.TEXT_PLAIN) public Response create(final String changesetData, @QueryParam("mapId") final String mapId) throws Exception { log.debug("Creating changeset for map with ID: " + mapId + " ..."); Document changesetDoc = null; try { log.debug("Parsing changeset XML..."); changesetDoc = XmlDocumentBuilder.parse(changesetData); } catch (Exception e) { ResourceErrorHandler.handleError("Error parsing changeset XML: " + StringUtils.abbreviate(changesetData, 100) + " (" + e.getMessage() + ")", Status.BAD_REQUEST, log); } Connection conn = DbUtils.createConnection(); long changesetId = -1; try { log.debug("Initializing database connection..."); long mapIdNum = -1; try { QMaps maps = QMaps.maps; //input mapId may be a map ID or a map name mapIdNum = ModelDaoUtils.getRecordIdForInputString(mapId, conn, maps, maps.id, maps.displayName); } catch (Exception e) { if (e.getMessage().startsWith("Multiple records exist")) { ResourceErrorHandler.handleError( e.getMessage().replaceAll("records", "maps").replaceAll("record", "map"), Status.NOT_FOUND, log); } else if (e.getMessage().startsWith("No record exists")) { ResourceErrorHandler.handleError( e.getMessage().replaceAll("records", "maps").replaceAll("record", "map"), Status.NOT_FOUND, log); } ResourceErrorHandler.handleError( "Error requesting map with ID: " + mapId + " (" + e.getMessage() + ")", Status.BAD_REQUEST, log); } long userId = -1; try { assert (mapIdNum != -1); log.debug("Retrieving user ID associated with map having ID: " + String.valueOf(mapIdNum) + " ..."); QMaps maps = QMaps.maps; SQLQuery query = new SQLQuery(conn, DbUtils.getConfiguration()); userId = query.from(maps).where(maps.id.eq(mapIdNum)).singleResult(maps.userId); log.debug("Retrieved user ID: " + userId); } catch (Exception e) { ResourceErrorHandler.handleError( "Error locating user associated with map for changeset data: " + StringUtils.abbreviate(changesetData, 100) + " (" + e.getMessage() + ")", Status.BAD_REQUEST, log); } log.debug("Intializing transaction..."); TransactionStatus transactionStatus = transactionManager .getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED)); conn.setAutoCommit(false); try { changesetId = Changeset.createChangeset(changesetDoc, mapIdNum, userId, conn); } catch (Exception e) { log.error("Rolling back the database transaction..."); transactionManager.rollback(transactionStatus); conn.rollback(); ResourceErrorHandler.handleError("Error creating changeset: (" + e.getMessage() + ") " + StringUtils.abbreviate(changesetData, 100), Status.BAD_REQUEST, log); } log.debug("Committing the database transaction..."); transactionManager.commit(transactionStatus); conn.commit(); } finally { conn.setAutoCommit(true); DbUtils.closeConnection(conn); } log.debug("Returning ID: " + changesetId + " for new changeset..."); return Response.ok(String.valueOf(changesetId), MediaType.TEXT_PLAIN) .header("Content-type", MediaType.TEXT_PLAIN).build(); } /** * <NAME>Changeset Upload</NAME> * <DESCRIPTION> * The Hootenanny Changeset Service implements a subset of the OSM * Changeset Service v0.6. It supports the OSM changeset upload process only. * It does not support the browsing of changeset contents. * </DESCRIPTION> * <PARAMETERS> * <changesetId> * long; ID of the changeset the changes should be uploaded into * </changesetId> * <changeset> * XML (payload data); a populated OSM xml changeset * </changeset> * </PARAMETERS> * <OUTPUT> * an OSM xml changeset upload response * </OUTPUT> * <EXAMPLE> * <URL>http://localhost:8080/hoot-services/osm/api/0.6/changeset/1/upload</URL> * <REQUEST_TYPE>POST</REQUEST_TYPE> * <INPUT> * OSM Changeset XML * </INPUT> * <OUTPUT> * an OSM xml changeset upload response * </OUTPUT> * </EXAMPLE> * Service method endpoint for creating a pre-flight request for uploading changeset diff data; * * required for CORS (http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support * @return Empty response with CORS headers */ @OPTIONS @Path("/{changesetId}/upload") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_PLAIN) public Response uploadPreflight() { log.debug("Handling changeset upload pre-flight request..."); return Response.ok().build(); } /** * Service method endpoint for uploading OSM changeset diff data * * @param changeset OSM changeset diff data * @param changesetId ID of the changeset being uploaded; changeset with the ID must already exist * @return response acknowledging the result of the update operation with updated entity ID * information * @throws Exception * @see http://wiki.openstreetmap.org/wiki/API_0.6 and * http://wiki.openstreetmap.org/wiki/OsmChange * @todo why can't I pass in changesetDiff as an XML doc instead of a string? */ @POST @Path("/{changesetId}/upload") @Consumes(MediaType.TEXT_XML) @Produces(MediaType.TEXT_XML) public Response upload(final String changeset, @PathParam("changesetId") final long changesetId, @QueryParam("mapId") final String mapId) throws Exception { Connection conn = DbUtils.createConnection(); Document changesetUploadResponse = null; try { log.debug("Intializing database connection..."); log.debug("Intializing changeset upload transaction..."); TransactionStatus transactionStatus = transactionManager .getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED)); conn.setAutoCommit(false); try { if (mapId == null) { throw new Exception("Invalid map id."); } long mapid = Long.parseLong(mapId); changesetUploadResponse = (new ChangesetDbWriter(conn)).write(mapid, changesetId, changeset); } catch (Exception e) { log.error("Rolling back transaction for changeset upload..."); transactionManager.rollback(transactionStatus); conn.rollback(); handleError(e, changesetId, StringUtils.abbreviate(changeset, 100)); } log.debug("Committing changeset upload transaction..."); transactionManager.commit(transactionStatus); conn.commit(); } finally { conn.setAutoCommit(true); DbUtils.closeConnection(conn); } log.debug("Returning changeset upload response: " + StringUtils.abbreviate(XmlDocumentBuilder.toString(changesetUploadResponse), 100) + " ..."); return Response.ok(new DOMSource(changesetUploadResponse), MediaType.TEXT_XML) .header("Content-type", MediaType.TEXT_XML).build(); } /** * Service method endpoint for creating a pre-flight request for the closing a changeset; required * for CORS (http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support * @return Empty response with CORS headers */ @OPTIONS @Path("/{changesetId}/close") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response closePreflight() { log.info("Handling changeset close pre-flight request..."); return Response.ok().build(); } /** * Service method endpoint for closing a changeset * * @param changeSetId ID of the changeset to close * @return HTTP status code indicating the status of the closing of the changeset * @throws Exception * @see http://wiki.openstreetmap.org/wiki/API_0.6 and */ @PUT @Path("/{changesetId}/close") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN) public String close(@PathParam("changesetId") final long changesetId, @QueryParam("mapId") final String mapId) throws Exception { log.info("Closing changeset with ID: " + changesetId + " ..."); Connection conn = DbUtils.createConnection(); try { log.debug("Intializing database connection..."); if (mapId == null) { throw new Exception("Invalid map id."); } long mapid = Long.parseLong(mapId); Changeset.closeChangeset(mapid, changesetId, conn); } finally { DbUtils.closeConnection(conn); } return Response.status(Status.OK).toString(); } public static void handleError(final Exception e, final long changesetId, final String changesetDiffSnippet) { if (!StringUtils.isEmpty(e.getMessage())) { if (e.getMessage().contains("Invalid changeset ID") || e.getMessage().contains("Invalid version") || e.getMessage().contains("references itself") || e.getMessage().contains("Changeset maximum element threshold exceeded") || e.getMessage().contains("was closed at")) { ResourceErrorHandler.handleError(e.getMessage(), Status.CONFLICT, log); //409 } else if (e.getMessage().contains("to be updated does not exist")) { ResourceErrorHandler.handleError(e.getMessage(), Status.NOT_FOUND, log); //404 } //TODO: should the visibility exception be changed from a 400 to a 409? else if (e.getMessage().contains("exist specified for") || e.getMessage().contains("exist for") || e.getMessage().contains("is still used by")) { ResourceErrorHandler.handleError(e.getMessage(), Status.PRECONDITION_FAILED, log); //412 } } //400 ResourceErrorHandler.handleError("Error uploading changeset with ID: " + changesetId + " - data: (" + e.getMessage() + ") " + changesetDiffSnippet, Status.BAD_REQUEST, log); } }