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) 2015, 2016 DigitalGlobe (http://www.digitalglobe.com/) */ package hoot.services.controllers.osm; import static hoot.services.models.db.QFolderMapMappings.folderMapMappings; import static hoot.services.models.db.QFolders.folders; import java.io.File; import java.io.StringWriter; import java.io.Writer; import java.net.SocketException; import java.sql.Connection; import java.sql.SQLException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.UUID; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.querydsl.core.Tuple; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.sql.Configuration; import com.querydsl.sql.SQLQuery; import com.querydsl.sql.dml.SQLDeleteClause; import com.querydsl.sql.dml.SQLInsertClause; import com.querydsl.sql.dml.SQLUpdateClause; import hoot.services.geo.BoundingBox; import hoot.services.job.JobExecutioner; import hoot.services.job.JobStatusManager; import hoot.services.models.db.FolderMapMappings; import hoot.services.models.db.Folders; import hoot.services.models.db.Maps; import hoot.services.models.db.QMaps; import hoot.services.models.db.QUsers; import hoot.services.models.db.Users; import hoot.services.models.osm.Element.ElementType; import hoot.services.models.osm.ElementFactory; import hoot.services.models.osm.Map; import hoot.services.models.osm.MapLayers; import hoot.services.models.osm.ModelDaoUtils; import hoot.services.utils.DbUtils; import hoot.services.utils.XmlDocumentBuilder; /** * Service endpoint for maps containing OSM data */ @Path("/api/0.6/map") public class MapResource { private static final Logger logger = LoggerFactory.getLogger(MapResource.class); private static final QMaps maps = QMaps.maps; public MapResource() { } /** * Returns a list of all map layers in the services database * * GET hoot-services/osm/api/0.6/map/layers * * @return a JSON object containing a list of map layers */ @GET @Path("/layers") @Produces(MediaType.APPLICATION_JSON) public MapLayers getLayers() { MapLayers mapLayers = null; try (Connection connection = DbUtils.createConnection()) { logger.info("Retrieving map layers list..."); List<Maps> mapLayerRecords = new SQLQuery<>(connection, DbUtils.getConfiguration()).select(maps) .from(maps).orderBy(maps.displayName.asc()).fetch(); mapLayers = Map.mapLayerRecordsToLayers(mapLayerRecords); } catch (Exception e) { handleError(e, null, null); } String message = "Returning map layers response"; if ((mapLayers != null) && (mapLayers.getLayers() != null)) { message += " of size: " + mapLayers.getLayers().length; } logger.debug(message); return mapLayers; } /** * <NAME>Map Service - List Folders </NAME> <DESCRIPTION> Returns a list of * all folders in the services database. </DESCRIPTION> <PARAMETERS> * </PARAMETERS> <OUTPUT> a JSON object containing a list of folders * </OUTPUT> <EXAMPLE> * <URL>http://localhost:8080/hoot-services/osm/api/0.6/map/ folders</URL> * <REQUEST_TYPE>GET</REQUEST_TYPE> <INPUT> </INPUT> <OUTPUT> { "folders": [ * { "id": 1, "name": "layer 1", "parentid":0, }, { "id": 2, "name": * "layer 2", "parentid":1, } ] } </OUTPUT> </EXAMPLE> * * Returns a list of all folders in the services database * * @return a JSON object containing a list of folders */ @GET @Path("/folders") @Produces(MediaType.APPLICATION_JSON) public FolderRecords getFolders() { FolderRecords folderRecords = null; try (Connection connection = DbUtils.createConnection()) { logger.info("Retrieving folders list..."); List<Folders> folderRecordSet = new SQLQuery<>(connection, DbUtils.getConfiguration()).select(folders) .from(folders).orderBy(folders.displayName.asc()).fetch(); folderRecords = mapFolderRecordsToFolders(folderRecordSet); } catch (Exception e) { handleError(e, null, null); } String message = "Returning map layers response"; if ((folderRecords != null) && (folderRecords.getFolders() != null)) { message += " of size: " + folderRecords.getFolders().length; } logger.debug(message); return folderRecords; } /** * Returns a list of all folders in the services database * * GET hoot-services/osm/api/0.6/map/links * * @return a JSON object containing a list of folders */ @GET @Path("/links") @Produces(MediaType.APPLICATION_JSON) public LinkRecords getLinks() { Configuration configuration = DbUtils.getConfiguration(); LinkRecords linkRecords = null; try (Connection connection = DbUtils.createConnection()) { logger.info("Retrieving links list..."); new SQLDeleteClause(connection, configuration, folderMapMappings) .where(new SQLQuery<>().from(maps).where(folderMapMappings.mapId.eq(maps.id)).notExists()) .execute(); try { new SQLInsertClause(connection, configuration, folderMapMappings) .columns(folderMapMappings.mapId, folderMapMappings.folderId) .select(new SQLQuery<>().select(maps.id, Expressions.numberTemplate(Long.class, "0")) .from(maps).where(maps.id.notIn(new SQLQuery<>().select(folderMapMappings.mapId) .distinct().from(folderMapMappings)))) .execute(); } catch (Exception e) { logger.error("Could not add missing records...", e); } List<FolderMapMappings> linkRecordSet = new SQLQuery<>(connection, configuration) .select(folderMapMappings).from(folderMapMappings).orderBy(folderMapMappings.folderId.asc()) .fetch(); linkRecords = mapLinkRecordsToLinks(linkRecordSet); } catch (Exception e) { handleError(e, null, null); } String message = "Returning links response"; logger.debug(message); return linkRecords; } private static Document generateExtentOSM(String maxlon, String maxlat, String minlon, String minlat) { SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); Date now = new Date(); String strDate = sdfDate.format(now); try { DocumentBuilderFactory dbf = XmlDocumentBuilder.getSecureDocBuilderFactory(); dbf.setValidating(false); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.newDocument(); Element osmElem = doc.createElement("osm"); osmElem.setAttribute("version", "0.6"); osmElem.setAttribute("generator", "hootenanny"); doc.appendChild(osmElem); Element boundsElem = doc.createElement("bounds"); boundsElem.setAttribute("minlat", minlat); boundsElem.setAttribute("minlon", minlon); boundsElem.setAttribute("maxlat", maxlat); boundsElem.setAttribute("maxlon", maxlon); osmElem.appendChild(boundsElem); // The ID's for these fabricated nodes were stepping on the ID's of actual nodes, so their ID's need to be // made negative and large, so they have no chance of stepping on anything. long node1Id = Long.MIN_VALUE + 3; long node2Id = Long.MIN_VALUE + 2; long node3Id = Long.MIN_VALUE + 1; long node4Id = Long.MIN_VALUE; Element nodeElem = doc.createElement("node"); nodeElem.setAttribute("id", String.valueOf(node1Id)); nodeElem.setAttribute("timestamp", strDate); nodeElem.setAttribute("user", "hootenannyuser"); nodeElem.setAttribute("visible", "true"); nodeElem.setAttribute("version", "1"); nodeElem.setAttribute("lat", maxlat); nodeElem.setAttribute("lon", minlon); osmElem.appendChild(nodeElem); nodeElem = doc.createElement("node"); nodeElem.setAttribute("id", String.valueOf(node2Id)); nodeElem.setAttribute("timestamp", strDate); nodeElem.setAttribute("user", "hootenannyuser"); nodeElem.setAttribute("visible", "true"); nodeElem.setAttribute("version", "1"); nodeElem.setAttribute("lat", maxlat); nodeElem.setAttribute("lon", maxlon); osmElem.appendChild(nodeElem); nodeElem = doc.createElement("node"); nodeElem.setAttribute("id", String.valueOf(node3Id)); nodeElem.setAttribute("timestamp", strDate); nodeElem.setAttribute("user", "hootenannyuser"); nodeElem.setAttribute("visible", "true"); nodeElem.setAttribute("version", "1"); nodeElem.setAttribute("lat", minlat); nodeElem.setAttribute("lon", maxlon); osmElem.appendChild(nodeElem); nodeElem = doc.createElement("node"); nodeElem.setAttribute("id", String.valueOf(node4Id)); nodeElem.setAttribute("timestamp", strDate); nodeElem.setAttribute("user", "hootenannyuser"); nodeElem.setAttribute("visible", "true"); nodeElem.setAttribute("version", "1"); nodeElem.setAttribute("lat", minlat); nodeElem.setAttribute("lon", minlon); osmElem.appendChild(nodeElem); Element wayElem = doc.createElement("way"); wayElem.setAttribute("id", String.valueOf(Long.MIN_VALUE)); wayElem.setAttribute("timestamp", strDate); wayElem.setAttribute("user", "hootenannyuser"); wayElem.setAttribute("visible", "true"); wayElem.setAttribute("version", "1"); Element ndElem = doc.createElement("nd"); ndElem.setAttribute("ref", String.valueOf(node1Id)); wayElem.appendChild(ndElem); ndElem = doc.createElement("nd"); ndElem.setAttribute("ref", String.valueOf(node2Id)); wayElem.appendChild(ndElem); ndElem = doc.createElement("nd"); ndElem.setAttribute("ref", String.valueOf(node3Id)); wayElem.appendChild(ndElem); ndElem = doc.createElement("nd"); ndElem.setAttribute("ref", String.valueOf(node4Id)); wayElem.appendChild(ndElem); ndElem = doc.createElement("nd"); ndElem.setAttribute("ref", String.valueOf(node1Id)); wayElem.appendChild(ndElem); /* * ndElem = doc.createElement("tag"); ndElem.setAttribute("k", "area"); * ndElem.setAttribute("v", "yes"); wayElem.appendChild(ndElem); */ osmElem.appendChild(wayElem); Transformer tf = TransformerFactory.newInstance().newTransformer(); // Fortify may require this, but it doesn't work. // TransformerFactory transformerFactory = // XmlDocumentBuilder.getSecureTransformerFactory(); tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); tf.setOutputProperty(OutputKeys.INDENT, "yes"); try (Writer out = new StringWriter()) { tf.transform(new DOMSource(doc), new StreamResult(out)); logger.debug("Layer Extent OSM: {}", out); } return doc; } catch (Exception e) { throw new RuntimeException("Error generating OSM extent", e); } } /** * GET hoot-services/osm/api/0.6/map?mapId=dc-admin&bbox * * @param mapId * ID of the map to query * @param BBox * geographic bounding box the requested entities should reside * in * @param multiLayerUniqueElementIds * if true, returned element IDs are prepended with <map * id>_<first letter of the element type>_; this setting * activated is not compatible with standard OSM clients * (specific to Hootenanny iD); defaults to false * @return response containing the data of the requested elements */ @GET @Produces(MediaType.TEXT_XML) public Response get(@QueryParam("mapId") String mapId, @QueryParam("bbox") String BBox, @QueryParam("extent") String extent, @QueryParam("autoextent") String auto, @DefaultValue("false") @QueryParam("multiLayerUniqueElementIds") boolean multiLayerUniqueElementIds) { Document responseDoc = null; try (Connection connection = DbUtils.createConnection()) { logger.info("Retrieving map data for map with ID: {} and bounds {} ...", mapId, BBox); long mapIdNum = -2; try { mapIdNum = Long.parseLong(mapId); } catch (NumberFormatException ignored) { // } if (mapIdNum == -1) { // OSM API database data can't be displayed on a hoot map, due to differences // between the display code, so we return no data here. responseDoc = writeEmptyResponse(); } else { String bbox = BBox; String[] Coords = bbox.split(","); if (Coords.length == 4) { String sMinX = Coords[0]; String sMinY = Coords[1]; String sMaxX = Coords[2]; String sMaxY = Coords[3]; double minX = Double.parseDouble(sMinX); double minY = Double.parseDouble(sMinY); double maxX = Double.parseDouble(sMaxX); double maxY = Double.parseDouble(sMaxY); minX = (minX > 180) ? 180 : minX; minX = (minX < -180) ? -180 : minX; maxX = (maxX > 180) ? 180 : maxX; maxX = (maxX < -180) ? -180 : maxX; minY = (minY > 90) ? 90 : minY; minY = (minY < -90) ? -90 : minY; maxY = (maxY > 90) ? 90 : maxY; maxY = (maxY < -90) ? -90 : maxY; bbox = minX + "," + minY + "," + maxX + "," + maxY; } mapIdNum = ModelDaoUtils.getRecordIdForInputString(mapId, connection, maps, maps.id, maps.displayName); BoundingBox queryBounds; try { queryBounds = new BoundingBox(bbox); logger.debug("Query bounds area: {}", queryBounds.getArea()); } catch (Exception e) { throw new RuntimeException( "Error parsing bounding box from bbox param: " + bbox + " (" + e.getMessage() + ")", e); } boolean doDefault = true; if ((auto != null) && (extent != null)) { if (auto.equalsIgnoreCase("manual")) { if (!extent.isEmpty()) { String[] coords = extent.split(","); if (coords.length == 4) { String maxlon = coords[0].trim(); String maxlat = coords[1].trim(); String minlon = coords[2].trim(); String minlat = coords[3].trim(); responseDoc = generateExtentOSM(maxlon, maxlat, minlon, minlat); doDefault = false; } } } } if (doDefault) { java.util.Map<ElementType, java.util.Map<Long, Tuple>> results = (new Map(mapIdNum, connection)) .query(queryBounds); responseDoc = writeResponse(results, queryBounds, multiLayerUniqueElementIds, mapIdNum, connection); } } } catch (Exception e) { handleError(e, mapId, BBox); } return Response.ok(new DOMSource(responseDoc)) .header("Content-Disposition", "attachment; filename=\"map.osm\"").build(); } @POST @Path("/nodescount") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public Response getTileNodesCounts(String params) { JSONObject ret = new JSONObject(); String mapId = ""; String bbox = ""; try (Connection connection = DbUtils.createConnection()) { JSONParser parser = new JSONParser(); JSONArray paramsArray = (JSONArray) parser.parse(params); long nodeCnt = 0; for (Object aParamsArray : paramsArray) { JSONObject param = (JSONObject) aParamsArray; mapId = (String) param.get("mapId"); long mapIdNum = -2; try { mapIdNum = Long.parseLong(mapId); } catch (NumberFormatException ignored) { // } // OSM API database data can't be displayed on a hoot map, due to differences // between the display code, so we return a zero count if its that layer. if (mapIdNum != -1) { logger.info("Retrieving node count for map with ID: {} ...", mapId); bbox = (String) param.get("tile"); String[] coords = bbox.split(","); if (coords.length == 4) { String sMinX = coords[0]; String sMinY = coords[1]; String sMaxX = coords[2]; String sMaxY = coords[3]; double minX = Double.parseDouble(sMinX); double minY = Double.parseDouble(sMinY); double maxX = Double.parseDouble(sMaxX); double maxY = Double.parseDouble(sMaxY); minX = (minX > 180) ? 180 : minX; minX = (minX < -180) ? -180 : minX; maxX = (maxX > 180) ? 180 : maxX; maxX = (maxX < -180) ? -180 : maxX; minY = (minY > 90) ? 90 : minY; minY = (minY < -90) ? -90 : minY; maxY = (maxY > 90) ? 90 : maxY; maxY = (maxY < -90) ? -90 : maxY; bbox = minX + "," + minY + "," + maxX + "," + maxY; } mapIdNum = ModelDaoUtils.getRecordIdForInputString(mapId, connection, maps, maps.id, maps.displayName); BoundingBox queryBounds; try { queryBounds = new BoundingBox(bbox); logger.debug("Query bounds area: {}", queryBounds.getArea()); } catch (Exception e) { throw new RuntimeException( "Error parsing bounding box from bbox param: " + bbox + " (" + e.getMessage() + ")", e); } Map currMap = new Map(mapIdNum, connection); nodeCnt += currMap.getNodesCount(queryBounds); } } ret.put("nodescount", nodeCnt); } catch (Exception e) { handleError(e, mapId, bbox); } return Response.ok(ret.toString()).build(); } @GET @Path("/mbr") @Produces(MediaType.APPLICATION_JSON) public Response getMBR(@QueryParam("mapId") String mapId) { JSONObject ret = new JSONObject(); try (Connection connection = DbUtils.createConnection()) { logger.info("Retrieving MBR for map with ID: {} ...", mapId); long mapIdNum = -2; try { mapIdNum = Long.parseLong(mapId); } catch (NumberFormatException ignored) { // } if (mapIdNum == -1) // OSM API db { // OSM API database data can't be displayed on a hoot map, due to differences // between the display code, so we arbitrarily returning roughly a CONUS bounds // here...not quite sure what else to return... ret.put("minlon", -110); ret.put("maxlon", -75); ret.put("minlat", 20); ret.put("maxlat", 50); ret.put("firstlon", 0); ret.put("firstlat", 0); ret.put("nodescount", 0); } else { mapIdNum = ModelDaoUtils.getRecordIdForInputString(mapId, connection, maps, maps.id, maps.displayName); BoundingBox queryBounds; try { queryBounds = new BoundingBox("-180,-90,180,90"); logger.debug("Query bounds area: {}", queryBounds.getArea()); } catch (Exception e) { throw new RuntimeException("Error parsing bounding box from bbox param: " + "-180,-90,180,90" + " (" + e.getMessage() + ")", e); } Map currMap = new Map(mapIdNum, connection); JSONObject extents = currMap.retrieveNodesMBR(queryBounds); if ((extents.get("minlat") == null) || (extents.get("maxlat") == null) || (extents.get("minlon") == null) || (extents.get("maxlon") == null)) { throw new Exception("Map is empty."); } JSONObject anode = currMap.retrieveANode(queryBounds); long nodeCnt = currMap.getNodesCount(queryBounds); double dMinLon = (Double) extents.get("minlon"); double dMaxLon = (Double) extents.get("maxlon"); double dMinLat = (Double) extents.get("minlat"); double dMaxLat = (Double) extents.get("maxlat"); double dFirstLon = (Double) anode.get("lon"); double dFirstLat = (Double) anode.get("lat"); ret.put("minlon", dMinLon); ret.put("maxlon", dMaxLon); ret.put("minlat", dMinLat); ret.put("maxlat", dMaxLat); ret.put("firstlon", dFirstLon); ret.put("firstlat", dFirstLat); ret.put("nodescount", nodeCnt); } } catch (Exception e) { handleError(e, mapId, "-180,-90,180,90"); } return Response.ok(ret.toString()).build(); } private static void handleError(Exception e, String mapId, String requestSnippet) { if ((e instanceof SocketException) && e.getMessage().toLowerCase().contains("broken pipe")) { // This occurs when iD aborts a tile request before it is finished. // This happens quite frequently but is acceptable, so let's catch this and just logger as // debug rather than an error to make the logs cleaner. logger.debug(e.getMessage()); } else if (!StringUtils.isEmpty(e.getMessage())) { if (e.getMessage().startsWith("Multiple records exist") || e.getMessage().startsWith("No record exists")) { String msg = e.getMessage().replaceAll("records", "maps").replaceAll("record", "map"); throw new WebApplicationException(e, Response.status(Status.NOT_FOUND).entity(msg).build()); } else if (e.getMessage().startsWith("Map is empty")) { String msg = e.getMessage(); throw new WebApplicationException(Response.status(Status.NOT_FOUND).entity(msg).build()); } else if (e.getMessage().startsWith("Error parsing bounding box from bbox param") || e.getMessage().contains("The maximum bbox size is") || e.getMessage().contains("The maximum number of nodes that may be returned in a map query")) { String msg = e.getMessage(); throw new WebApplicationException(e, Response.status(Status.BAD_REQUEST).entity(msg).build()); } } else { if (mapId != null) { String msg = "Error querying map with ID: " + mapId + " - data: " + requestSnippet; throw new WebApplicationException(e, Response.serverError().entity(msg).build()); } else { String msg = "Error listing layers for map - data: " + requestSnippet; throw new WebApplicationException(e, Response.serverError().entity(msg).build()); } } String msg = e.getMessage(); throw new WebApplicationException(e, Response.serverError().entity(msg).build()); } /** * Deletes a map * * POST hoot-services/osm/api/0.6/map/delete?mapId={Map ID} * * //TODO: should be an HTTP DELETE * * @param mapId * ID of map record to be deleted * @return id of the deleted map */ @POST @Path("/delete") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public Response deleteLayers(@QueryParam("mapId") String mapId) { JSONObject command = new JSONObject(); command.put("mapId", mapId); command.put("execImpl", "resourcesCleanUtil"); String jobId = UUID.randomUUID().toString(); (new JobExecutioner(jobId, command)).start(); JSONObject json = new JSONObject(); json.put("jobId", jobId); return Response.ok(json.toJSONString()).build(); } /** * * POST * hoot-services/osm/api/0.6/map/modify?mapId=123456&inputType='Dataset'& * modName='New Dataset' * * //TODO: should be an HTTP PUT * * @param mapId * ID of map record or folder to be modified * @param modName * The new name for the dataset * @param inputType * Flag for either dataset or folder * @return jobId Success = True/False */ @POST @Path("/modify") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public Response modifyName(@QueryParam("mapId") String mapId, @QueryParam("modName") String modName, @QueryParam("inputType") String inputType) { try (Connection connection = DbUtils.createConnection()) { Configuration configuration = DbUtils.getConfiguration(); if (inputType.toLowerCase(Locale.ENGLISH).equals("dataset")) { new SQLUpdateClause(connection, configuration, maps).where(maps.id.eq(Long.valueOf(mapId))) .set(maps.displayName, modName).execute(); logger.debug("Renamed map with id {} {}...", mapId, modName); } else if (inputType.toLowerCase(Locale.ENGLISH).equals("folder")) { new SQLUpdateClause(connection, configuration, folders).where(folders.id.eq(Long.valueOf(mapId))) .set(folders.displayName, modName).execute(); logger.debug("Renamed folder with id {} {}...", mapId, modName); } } catch (Exception e) { handleError(e, null, null); } JSONObject json = new JSONObject(); json.put("success", true); return Response.ok(json.toJSONString()).build(); } /** * Adds new dataset folder * * POST hoot-services/osm/api/0.6/map/addfolder?folderName={foldername}& * parentId= {parentId} * * @param folderName * Display name of folder * @param parentId * The parent folder of the new folder. If at root level, is * equal to 0. * @return jobId Success = True/False */ @POST @Path("/addfolder") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public Response addFolder(@QueryParam("folderName") String folderName, @QueryParam("parentId") Long parentId) { Long newId = -1L; try (Connection connection = DbUtils.createConnection()) { Configuration configuration = DbUtils.getConfiguration(); List<Long> ids = new SQLQuery<>(connection, configuration) .select(Expressions.numberTemplate(Long.class, "nextval('folders_id_seq')")).from().fetch(); if ((ids != null) && (!ids.isEmpty())) { newId = ids.get(0); Timestamp now = new Timestamp(Calendar.getInstance().getTimeInMillis()); long userId = 1; new SQLInsertClause(connection, configuration, folders) .columns(folders.id, folders.createdAt, folders.displayName, folders.publicCol, folders.userId, folders.parentId) .values(newId, now, folderName, true, userId, parentId).execute(); } } catch (Exception e) { handleError(e, null, null); } JSONObject json = new JSONObject(); json.put("success", true); json.put("folderId", newId); return Response.ok(json.toJSONString()).build(); } /** * Deletes a dataset folder. * * POST hoot-services/osm/api/0.6/map/deletefolder?folderId={folderId} * * //TODO: should be an HTTP DELETE * * @param folderId * Folder Id * @return jobId */ @POST @Path("/deletefolder") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public Response deleteFolder(@QueryParam("folderId") Long folderId) { try (Connection connection = DbUtils.createConnection()) { Configuration configuration = DbUtils.getConfiguration(); List<Long> parentId = new SQLQuery<>(connection, configuration).select(folders.id).from(folders) .where(folders.id.eq(folderId)).fetch(); Long prntId = !parentId.isEmpty() ? parentId.get(0) : 0L; new SQLUpdateClause(connection, configuration, folders).where(folders.parentId.eq(folderId)) .set(folders.parentId, prntId).execute(); new SQLDeleteClause(connection, configuration, folders).where(folders.id.eq(folderId)).execute(); new SQLUpdateClause(connection, configuration, folderMapMappings) .where(folderMapMappings.folderId.eq(folderId)).set(folderMapMappings.folderId, 0L).execute(); } catch (Exception e) { handleError(e, null, null); } JSONObject json = new JSONObject(); json.put("success", true); return Response.ok(json.toJSONString()).build(); } /** * * POST hoot-services/osm/api/0.6/map/updateParentId?folderId={folderId} * * //TODO: should be an HTTP PUT * * @param folderId * ID of folder * @param parentId * ? * @param newRecord * ? * @return jobId Success = True/False */ @POST @Path("/updateParentId") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public Response updateParentId(@QueryParam("folderId") Long folderId, @QueryParam("parentId") Long parentId, @QueryParam("newRecord") Boolean newRecord) { try (Connection connection = DbUtils.createConnection()) { new SQLUpdateClause(connection, DbUtils.getConfiguration(), folders).where(folders.id.eq(folderId)) .set(folders.parentId, parentId).execute(); } catch (Exception e) { handleError(e, null, null); } JSONObject json = new JSONObject(); json.put("success", true); return Response.ok(json.toJSONString()).build(); } /** * Adds or modifies record in folder_map_mappings if layer is created or * modified * * @param folderId * ID of folder * @param mapId * ID of map * @param updateType * new: creates new link; update: updates link delete: deletes * link * @return jobId Success = True/False */ @POST @Path("/linkMapFolder") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public Response updateFolderMapLink(@QueryParam("folderId") Long folderId, @QueryParam("mapId") Long mapId, @QueryParam("updateType") String updateType) { try (Connection conn = DbUtils.createConnection()) { Configuration configuration = DbUtils.getConfiguration(); // Delete any existing to avoid duplicate entries new SQLDeleteClause(conn, configuration, folderMapMappings).where(folderMapMappings.mapId.eq(mapId)) .execute(); if (updateType.equalsIgnoreCase("new") || updateType.equalsIgnoreCase("update")) { List<Long> ids = new SQLQuery<>(conn, configuration) .select(Expressions.numberTemplate(Long.class, "nextval('folder_map_mappings_id_seq')")) .from().fetch(); if ((ids != null) && (!ids.isEmpty())) { Long newId = ids.get(0); new SQLInsertClause(conn, configuration, folderMapMappings) .columns(folderMapMappings.id, folderMapMappings.mapId, folderMapMappings.folderId) .values(newId, mapId, folderId).execute(); } } } catch (Exception e) { handleError(e, null, null); } JSONObject json = new JSONObject(); json.put("success", true); return Response.ok(json.toJSONString()).build(); } public String updateTagsDirect(java.util.Map<String, String> tags, String mapName) throws SQLException { // _zoomLevels String jobId = UUID.randomUUID().toString(); try (Connection connection = DbUtils.createConnection()) { JobStatusManager jobStatusManager = null; try { // Currently we do not have any way to get map id directly from hoot // core command when it runs so for now we need get the all the map ids matching name and pick // first one.. // THIS WILL NEED TO CHANGE when we implement handle map by Id // instead of name.. List<Long> mapIds = DbUtils.getMapIdsByName(connection, mapName); if (!mapIds.isEmpty()) { // we are expecting the last one of duplicate name to be the one // resulted from the conflation // This can be wrong if there is race condition. REMOVE THIS // once core // implement map Id return long mapId = mapIds.get(mapIds.size() - 1); jobStatusManager = new JobStatusManager(connection); jobStatusManager.addJob(jobId); // Hack alert! // Add special handling of stats tag key // We need to read the file in here, because the file doesn't // exist at the time // the updateMapsTagsCommand job is created in // ConflationResource.java String statsKey = "stats"; if (tags.containsKey(statsKey)) { String statsName = tags.get(statsKey); File statsFile = new File(statsName); if (statsFile.exists()) { logger.debug("Found {}", statsName); String stats = FileUtils.readFileToString(statsFile, "UTF-8"); tags.put(statsKey, stats); if (!statsFile.delete()) { logger.error("Error deleting {} file", statsFile.getAbsolutePath()); } } else { logger.error("Can't find {}", statsName); tags.remove(statsKey); } } DbUtils.updateMapsTableTags(tags, mapId, connection); jobStatusManager.setComplete(jobId); } } catch (Exception ex) { if (jobStatusManager != null) { jobStatusManager.setFailed(jobId, ex.getMessage()); } String msg = "Failure update map tags resource" + ex.getMessage(); throw new WebApplicationException(ex, Response.serverError().entity(msg).build()); } } return jobId; } @GET @Path("/tags") @Produces(MediaType.APPLICATION_JSON) public Response getTags(@QueryParam("mapid") String mapId) { JSONObject json = new JSONObject(); try (Connection connection = DbUtils.createConnection()) { if (!StringUtils.isEmpty(mapId)) { long mapIdNum = -2; try { mapIdNum = Long.parseLong(mapId); } catch (NumberFormatException ignored) { // a map name string could also be passed in here } if (mapIdNum != -1) // not OSM API db { mapIdNum = ModelDaoUtils.getRecordIdForInputString(mapId, connection, maps, maps.id, maps.displayName); logger.info("Retrieving map tags for map with ID: {} ...", mapIdNum); try { java.util.Map<String, String> tags = DbUtils.getMapsTableTags(mapIdNum, connection); if (tags != null) { logger.debug(tags.toString()); } json.putAll(tags); Object oInput1 = json.get("input1"); if (oInput1 != null) { String dispName = DbUtils.getDisplayNameById(connection, Long.valueOf(oInput1.toString())); json.put("input1Name", dispName); } Object oInput2 = json.get("input2"); if (oInput2 != null) { String dispName = DbUtils.getDisplayNameById(connection, Long.valueOf(oInput2.toString())); json.put("input2Name", dispName); } } catch (Exception e) { throw new RuntimeException("Error getting map tags. :" + e.getMessage(), e); } } } } catch (Exception e) { handleError(e, mapId, ""); } return Response.ok(json.toString()).build(); } public static long validateMap(String mapId, Connection conn) { long mapIdNum; try { // input mapId may be a map ID or a map name mapIdNum = ModelDaoUtils.getRecordIdForInputString(mapId, conn, maps, maps.id, maps.displayName); } catch (Exception ex) { if (ex.getMessage().startsWith("Multiple records exist") || ex.getMessage().startsWith("No record exists")) { String msg = ex.getMessage().replaceAll("records", "maps").replaceAll("record", "map"); throw new WebApplicationException(ex, Response.status(Status.NOT_FOUND).entity(msg).build()); } else { String msg = "Error requesting map with ID: " + mapId + " (" + ex.getMessage() + ")"; throw new WebApplicationException(ex, Response.status(Status.BAD_REQUEST).entity(msg).build()); } } return mapIdNum; } /** * Writes a map query response with no element data * * @return a XML document response */ private static Document writeEmptyResponse() { Document responseDoc = null; try { responseDoc = XmlDocumentBuilder.create(); } catch (ParserConfigurationException e) { throw new RuntimeException("Error creating XmlDocumentBuilder!", e); } Element elementRootXml = OsmResponseHeaderGenerator.getOsmDataHeader(responseDoc); responseDoc.appendChild(elementRootXml); return responseDoc; } /** * Writes the query response to an XML document * * @param results * query results; a mapping of element IDs to records, grouped by * element type * @param queryBounds * bounds of the query * @param multiLayerUniqueElementIds * if true, IDs are prepended with <map id>_<first letter of the * element type>_; this setting activated is not compatible with * standard OSM clients (specific to Hootenanny iD) * @return an XML document */ private static Document writeResponse(java.util.Map<ElementType, java.util.Map<Long, Tuple>> results, BoundingBox queryBounds, boolean multiLayerUniqueElementIds, long mapId, Connection connection) { Document responseDoc = null; try { responseDoc = XmlDocumentBuilder.create(); } catch (ParserConfigurationException e) { throw new RuntimeException("Error creating XmlDocumentBuilder!", e); } Element elementRootXml = OsmResponseHeaderGenerator.getOsmDataHeader(responseDoc); responseDoc.appendChild(elementRootXml); if (!results.isEmpty()) { elementRootXml.appendChild(queryBounds.toXml(elementRootXml)); for (ElementType elementType : ElementType.values()) { if (elementType != ElementType.Changeset) { java.util.Map<Long, Tuple> resultsForType = results.get(elementType); if (resultsForType != null) { for (java.util.Map.Entry<Long, Tuple> entry : resultsForType.entrySet()) { Tuple record = entry.getValue(); hoot.services.models.osm.Element element = ElementFactory.create(elementType, record, connection, mapId); // the query that sent this in should have // already handled filtering out invisible elements Users usersTable = record.get(QUsers.users); Element elementXml = element.toXml(elementRootXml, usersTable.getId(), usersTable.getDisplayName(), multiLayerUniqueElementIds, true); elementRootXml.appendChild(elementXml); } } } } } return responseDoc; } /** * Converts a set of folder database records into an object returnable by a * web service * * @param folderRecordSet * set of map layer records * @return folders web service object */ private static FolderRecords mapFolderRecordsToFolders(List<Folders> folderRecordSet) { FolderRecords folderRecords = new FolderRecords(); List<FolderRecord> folderRecordList = new ArrayList<>(); for (Folders folderRecord : folderRecordSet) { FolderRecord folder = new FolderRecord(); folder.setId(folderRecord.getId()); folder.setName(folderRecord.getDisplayName()); folder.setParentId(folderRecord.getParentId()); folderRecordList.add(folder); } folderRecords.setFolders(folderRecordList.toArray(new FolderRecord[folderRecordList.size()])); return folderRecords; } /** * Converts a set of database records into an object returnable by a web * service * * @param linkRecordSet * set of map layer records * @return folders web service object */ private static LinkRecords mapLinkRecordsToLinks(List<FolderMapMappings> linkRecordSet) { LinkRecords linkRecords = new LinkRecords(); List<LinkRecord> linkRecordList = new ArrayList<>(); for (FolderMapMappings linkRecord : linkRecordSet) { LinkRecord link = new LinkRecord(); link.setId(linkRecord.getId()); link.setFolderId(linkRecord.getFolderId()); link.setMapId(linkRecord.getMapId()); linkRecordList.add(link); } linkRecords.setLinks(linkRecordList.toArray(new LinkRecord[linkRecordList.size()])); return linkRecords; } }