Java tutorial
/* * Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package amazon.dynamodb.config; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import com.amazonaws.AmazonClientException; import amazon.dynamodb.internal.S2Manager; import amazon.dynamodb.internal.S2Util; import amazon.dynamodb.model.BatchWritePointResult; import amazon.dynamodb.model.DeletePointRequest; import amazon.dynamodb.model.DeletePointResult; import amazon.dynamodb.model.GeoHashRango; import amazon.dynamodb.model.GeoPunto; import amazon.dynamodb.model.GeoQueryRequest; import amazon.dynamodb.model.GeoQueryResult; import amazon.dynamodb.model.GetPointRequest; import amazon.dynamodb.model.GetPointResult; import amazon.dynamodb.model.PutPointRequest; import amazon.dynamodb.model.PutPointResult; import amazon.dynamodb.model.QueryRadiusRequest; import amazon.dynamodb.model.QueryRadiusResult; import amazon.dynamodb.model.QueryRectangleRequest; import amazon.dynamodb.model.QueryRectangleResult; import amazon.dynamodb.model.UpdatePointRequest; import amazon.dynamodb.model.UpdatePointResult; import amazon.dynamodb.util.GeoJsonMapper; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.QueryRequest; import com.amazonaws.services.dynamodbv2.model.QueryResult; import com.google.common.geometry.S2CellId; import com.google.common.geometry.S2CellUnion; import com.google.common.geometry.S2LatLng; import com.google.common.geometry.S2LatLngRect; /** * <p> * Manager to hangle geo spatial data in Amazon DynamoDB tables. All service * calls made using this client are blocking, and will not return until the * service call completes. * </p> * <p> * This class is designed to be thread safe; however, once constructed * GeoDataManagerConfiguration should not be modified. Modifying * GeoDataManagerConfiguration may cause unspecified behaviors. * </p> * */ public class GeoDataManager { private GeoDataManagerConfiguration config; private DynamoDBManager dynamoDBManager; /** * <p> * Construct and configure GeoDataManager using GeoDataManagerConfiguration. * </p> * <b>Sample usage:</b> * * <pre> * AmazonDynamoDBClient ddb = new AmazonDynamoDBClient(new ClasspathPropertiesFileCredentialsProvider()); * Region usWest2 = Region.getRegion(Regions.US_WEST_2); * ddb.setRegion(usWest2); * * ClientConfiguration clientConfiguration = new ClientConfiguration().withMaxErrorRetry(5); * ddb.setConfiguration(clientConfiguration); * * GeoDataManagerConfiguration config = new GeoDataManagerConfiguration(ddb, "geo-table"); * GeoDataManager geoDataManager = new GeoDataManager(config); * </pre> * * @param config Container for the configuration parameters for * GeoDataManager. */ public GeoDataManager(GeoDataManagerConfiguration config) { this.config = config; dynamoDBManager = new DynamoDBManager(this.config); } /** * <p> * Return GeoDataManagerConfiguration. The returned * GeoDataManagerConfiguration should not be modified. * </p> * * @return GeoDataManagerConfiguration that is used to configure this * GeoDataManager. */ public GeoDataManagerConfiguration getGeoDataManagerConfiguration() { return config; } /** * <p> * Put a point into the Amazon DynamoDB table. Once put, you cannot update * attributes specified in GeoDataManagerConfiguration: hash key, range key, * geohash and geoJson. If you want to update these columns, you need to * insert a new record and delete the old record. * </p> * <b>Sample usage:</b> * * <pre> * GeoPoint geoPoint = new GeoPoint(47.5, -122.3); * AttributeValue rangeKeyValue = new AttributeValue().withS("a6feb446-c7f2-4b48-9b3a-0f87744a5047"); * AttributeValue titleValue = new AttributeValue().withS("Original title"); * * PutPointRequest putPointRequest = new PutPointRequest(geoPoint, rangeKeyValue); * putPointRequest.getPutItemRequest().getItem().put("title", titleValue); * * PutPointResult putPointResult = geoDataManager.putPoint(putPointRequest); * </pre> * * @param putPointRequest Container for the necessary parameters to execute * put point request. * * @return Result of put point request. */ public PutPointResult putPoint(PutPointRequest putPointRequest) { return dynamoDBManager.putPoint(putPointRequest); } /** * <p> * Put a list of points into the Amazon DynamoDB table. Once put, you cannot * update attributes specified in GeoDataManagerConfiguration: hash key, * range key, geohash and geoJson. If you want to update these columns, you * need to insert a new record and delete the old record. * </p> * <b>Sample usage:</b> * * <pre> * GeoPoint geoPoint = new GeoPoint(47.5, -122.3); * AttributeValue rangeKeyValue = new AttributeValue().withS("a6feb446-c7f2-4b48-9b3a-0f87744a5047"); * AttributeValue titleValue = new AttributeValue().withS("Original title"); * * PutPointRequest putPointRequest = new PutPointRequest(geoPoint, rangeKeyValue); * putPointRequest.getPutItemRequest().getItem().put("title", titleValue); * List<PutPointRequest> putPointRequests = new ArrayList<PutPointRequest>(); * putPointRequests.add(putPointRequest); * BatchWritePointResult batchWritePointResult = geoDataManager.batchWritePoints(putPointRequests); * </pre> * * @param putPointRequests Container for the necessary parameters to execute * put point request. * * @return Result of batch put point request. */ public BatchWritePointResult batchWritePoints(List<PutPointRequest> putPointRequests) { return dynamoDBManager.batchWritePoints(putPointRequests); } /** * <p> * Get a point from the Amazon DynamoDB table. * </p> * <b>Sample usage:</b> * * <pre> * GeoPoint geoPoint = new GeoPoint(47.5, -122.3); * AttributeValue rangeKeyValue = new AttributeValue().withS("a6feb446-c7f2-4b48-9b3a-0f87744a5047"); * * GetPointRequest getPointRequest = new GetPointRequest(geoPoint, rangeKeyValue); * GetPointResult getPointResult = geoIndexManager.getPoint(getPointRequest); * * System.out.println("item: " + getPointResult.getGetItemResult().getItem()); * </pre> * * @param getPointRequest Container for the necessary parameters to execute * get point request. * * @return Result of get point request. * */ public GetPointResult getPoint(GetPointRequest getPointRequest) { return dynamoDBManager.getPoint(getPointRequest); } /** * <p> * Query a rectangular area constructed by two points and return all points * within the area. Two points need to construct a rectangle from minimum * and maximum latitudes and longitudes. If minPoint.getLongitude() > * maxPoint.getLongitude(), the rectangle spans the 180 degree longitude * line. * </p> * <b>Sample usage:</b> * * <pre> * GeoPoint minPoint = new GeoPoint(45.5, -124.3); * GeoPoint maxPoint = new GeoPoint(49.5, -120.3); * * QueryRectangleRequest queryRectangleRequest = new QueryRectangleRequest(minPoint, maxPoint); * QueryRectangleResult queryRectangleResult = geoIndexManager.queryRectangle(queryRectangleRequest); * * for (Map<String, AttributeValue> item : queryRectangleResult.getItem()) { * System.out.println("item: " + item); * } * </pre> * * @param queryRectangleRequest Container for the necessary parameters to * execute rectangle query request. * * @return Result of rectangle query request. */ public QueryRectangleResult queryRectangle(QueryRectangleRequest queryRectangleRequest) { S2LatLngRect latLngRect = S2Util.getBoundingLatLngRect(queryRectangleRequest); S2CellUnion cellUnion = S2Manager.findCellIds(latLngRect); List<GeoHashRango> ranges = mergeCells(cellUnion); cellUnion = null; return new QueryRectangleResult(dispatchQueries(ranges, queryRectangleRequest)); } /** * <p> * Query a circular area constructed by a center point and its radius. * </p> * <b>Sample usage:</b> * * <pre> * GeoPoint centerPoint = new GeoPoint(47.5, -122.3); * * QueryRadiusRequest queryRadiusRequest = new QueryRadiusRequest(centerPoint, 100); * QueryRadiusResult queryRadiusResult = geoIndexManager.queryRadius(queryRadiusRequest); * * for (Map<String, AttributeValue> item : queryRadiusResult.getItem()) { * System.out.println("item: " + item); * } * </pre> * * @param queryRadiusRequest Container for the necessary parameters to * execute radius query request. * * @return Result of radius query request. * */ public QueryRadiusResult queryRadius(QueryRadiusRequest queryRadiusRequest) { S2LatLngRect latLngRect = S2Util.getBoundingLatLngRect(queryRadiusRequest); S2CellUnion cellUnion = S2Manager.findCellIds(latLngRect); List<GeoHashRango> ranges = mergeCells(cellUnion); cellUnion = null; return new QueryRadiusResult(dispatchQueries(ranges, queryRadiusRequest)); } /** * <p> * Update a point data in Amazon DynamoDB table. You cannot update * attributes specified in GeoDataManagerConfiguration: hash key, range key, * geohash and geoJson. If you want to update these columns, you need to * insert a new record and delete the old record. * </p> * <b>Sample usage:</b> * * <pre> * GeoPoint geoPoint = new GeoPoint(47.5, -122.3); * * String rangeKey = "a6feb446-c7f2-4b48-9b3a-0f87744a5047"; * AttributeValue rangeKeyValue = new AttributeValue().withS(rangeKey); * * UpdatePointRequest updatePointRequest = new UpdatePointRequest(geoPoint, rangeKeyValue); * * AttributeValue titleValue = new AttributeValue().withS("Updated title."); * AttributeValueUpdate titleValueUpdate = new AttributeValueUpdate().withAction(AttributeAction.PUT) * .withValue(titleValue); * updatePointRequest.getUpdateItemRequest().getAttributeUpdates().put("title", titleValueUpdate); * * UpdatePointResult updatePointResult = geoIndexManager.updatePoint(updatePointRequest); * </pre> * * @param updatePointRequest Container for the necessary parameters to * execute update point request. * * @return Result of update point request. */ public UpdatePointResult updatePoint(UpdatePointRequest updatePointRequest) { return dynamoDBManager.updatePoint(updatePointRequest); } /** * <p> * Delete a point from the Amazon DynamoDB table. * </p> * <b>Sample usage:</b> * * <pre> * GeoPoint geoPoint = new GeoPoint(47.5, -122.3); * * String rangeKey = "a6feb446-c7f2-4b48-9b3a-0f87744a5047"; * AttributeValue rangeKeyValue = new AttributeValue().withS(rangeKey); * * DeletePointRequest deletePointRequest = new DeletePointRequest(geoPoint, rangeKeyValue); * DeletePointResult deletePointResult = geoIndexManager.deletePoint(deletePointRequest); * </pre> * * @param deletePointRequest Container for the necessary parameters to * execute delete point request. * * @return Result of delete point request. */ public DeletePointResult deletePoint(DeletePointRequest deletePointRequest) { return dynamoDBManager.deletePoint(deletePointRequest); } /** * Merge continuous cells in cellUnion and return a list of merged * GeoHashRangos. * * @param cellUnion Container for multiple cells. * * @return A list of merged GeoHashRangos. */ private List<GeoHashRango> mergeCells(S2CellUnion cellUnion) { List<GeoHashRango> ranges = new ArrayList<GeoHashRango>(); for (S2CellId c : cellUnion.cellIds()) { GeoHashRango range = new GeoHashRango(c.rangeMin().id(), c.rangeMax().id()); boolean wasMerged = false; for (GeoHashRango r : ranges) { if (r.tryMerge(range)) { wasMerged = true; break; } } if (!wasMerged) { ranges.add(range); } } return ranges; } /** * Query Amazon DynamoDB in parallel and filter the result. * * @param ranges A list of geohash ranges that will be used to query Amazon * DynamoDB. * * @param latLngRect The rectangle area that will be used as a reference * point for precise filtering. * * @return Aggregated and filtered items returned from Amazon DynamoDB. */ private GeoQueryResult dispatchQueries(List<GeoHashRango> ranges, GeoQueryRequest geoQueryRequest) { GeoQueryResult geoQueryResult = new GeoQueryResult(); ExecutorService executorService = config.getExecutorService(); List<Future<?>> futureList = new ArrayList<Future<?>>(); for (GeoHashRango outerRange : ranges) { for (GeoHashRango range : outerRange.trySplit(config.getHashKeyLength())) { GeoQueryThread geoQueryThread = new GeoQueryThread(geoQueryRequest, geoQueryResult, range); futureList.add(executorService.submit(geoQueryThread)); } } ranges = null; for (int i = 0; i < futureList.size(); i++) { try { futureList.get(i).get(); } catch (Exception e) { for (int j = i + 1; j < futureList.size(); j++) { futureList.get(j).cancel(true); } throw new AmazonClientException("Querying Amazon DynamoDB failed.", e); } } futureList = null; return geoQueryResult; } /** * Filter out any points outside of the queried area from the input list. * * @param list List of items return by Amazon DynamoDB. It may contains * points outside of the actual area queried. * * @param latLngRect Queried area. Any points outside of this area need to * be discarded. * * @return List of items within the queried area. */ private List<Map<String, AttributeValue>> filter(List<Map<String, AttributeValue>> list, GeoQueryRequest geoQueryRequest) { List<Map<String, AttributeValue>> result = new ArrayList<Map<String, AttributeValue>>(); S2LatLngRect latLngRect = null; S2LatLng centerLatLng = null; double radiusInMeter = 0; if (geoQueryRequest instanceof QueryRectangleRequest) { latLngRect = S2Util.getBoundingLatLngRect(geoQueryRequest); } else if (geoQueryRequest instanceof QueryRadiusRequest) { GeoPunto centerPoint = ((QueryRadiusRequest) geoQueryRequest).getCenterPoint(); centerLatLng = S2LatLng.fromDegrees(centerPoint.getLatitud(), centerPoint.getLongitud()); radiusInMeter = ((QueryRadiusRequest) geoQueryRequest).getRadiusInMeter(); } for (Map<String, AttributeValue> item : list) { String geoJson = item.get(config.getGeoJsonAttributeName()).getS(); GeoPunto geoPoint = GeoJsonMapper.geoPointFromString(geoJson); S2LatLng latLng = S2LatLng.fromDegrees(geoPoint.getLatitud(), geoPoint.getLongitud()); if (latLngRect != null && latLngRect.contains(latLng)) { result.add(item); } else if (centerLatLng != null && radiusInMeter > 0 && centerLatLng.getEarthDistance(latLng) <= radiusInMeter) { result.add(item); } } return result; } /** * Worker thread to query Amazon DynamoDB. * */ private class GeoQueryThread extends Thread { private GeoQueryRequest geoQueryRequest; private GeoQueryResult geoQueryResult; private GeoHashRango range; public GeoQueryThread(GeoQueryRequest geoQueryRequest, GeoQueryResult geoQueryResult, GeoHashRango range) { this.geoQueryRequest = geoQueryRequest; this.geoQueryResult = geoQueryResult; this.range = range; } public void run() { QueryRequest queryRequest = DynamoDBUtil.copyQueryRequest(geoQueryRequest.getQueryRequest()); long hashKey = S2Manager.generateHashKey(range.getRangeMin(), config.getHashKeyLength()); List<QueryResult> queryResults = dynamoDBManager.queryGeohash(queryRequest, hashKey, range); for (QueryResult queryResult : queryResults) { if (isInterrupted()) { return; } // getQueryResults() returns a synchronized list. geoQueryResult.getQueryResults().add(queryResult); List<Map<String, AttributeValue>> filteredQueryResult = filter(queryResult.getItems(), geoQueryRequest); // getItem() returns a synchronized list. geoQueryResult.getItem().addAll(filteredQueryResult); } } } }