diff --git a/.gitignore b/.gitignore index ec7e95c..695ec70 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,14 @@ com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties +.idea/ +examples/attribute-security-filter-soi/attribute-security-filter-soi.iml +examples/attribute-security-filter-soi/target/ +examples/clustering-soe/clustering-soe.iml +examples/clustering-soe/target/ +server-extension-core/server-extension-core.iml +server-extension-core/target/ +server-extension-java.iml +server-extension-template/server-extension-template.iml +server-extension-template/target/ +target/ diff --git a/examples/attribute-security-filter-soi/attribute-security-filter-soi.iml b/examples/attribute-security-filter-soi/attribute-security-filter-soi.iml deleted file mode 100644 index 82aab0b..0000000 --- a/examples/attribute-security-filter-soi/attribute-security-filter-soi.iml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - file://$MODULE_DIR$/src/main/java/com/esri/arcgis/soi/attributesecurityfilter/AttributeSecurityFilterConfig.java - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/attribute-security-filter-soi/pom.xml b/examples/attribute-security-filter-soi/pom.xml index e08977d..9f758e1 100644 --- a/examples/attribute-security-filter-soi/pom.xml +++ b/examples/attribute-security-filter-soi/pom.xml @@ -24,14 +24,14 @@ attribute-security-filter-soi attribute-security-filter-soi - A Server Object Interceptor (SOI) sample to enable feature-level security for query operations + A Server Object Interceptor (SOI) example to enable feature-level security for query operations jar com.esri.serverextensions server-extension-core - ${parent.version} + ${project.parent.version} diff --git a/examples/attribute-security-filter-soi/src/assembly/soe-assembly.xml b/examples/attribute-security-filter-soi/src/assembly/soe-assembly.xml index 536d97f..434b784 100644 --- a/examples/attribute-security-filter-soi/src/assembly/soe-assembly.xml +++ b/examples/attribute-security-filter-soi/src/assembly/soe-assembly.xml @@ -1,3 +1,4 @@ + - ${project.name} ${project.description} diff --git a/examples/attribute-security-filter-soi/src/main/java/com/esri/serverextension/attributesecurityfilter/AttributeSecurityFilterConfig.java b/examples/attribute-security-filter-soi/src/main/java/com/esri/serverextension/attributesecurityfilter/AttributeSecurityFilterConfig.java index 8526d67..656b1c4 100644 --- a/examples/attribute-security-filter-soi/src/main/java/com/esri/serverextension/attributesecurityfilter/AttributeSecurityFilterConfig.java +++ b/examples/attribute-security-filter-soi/src/main/java/com/esri/serverextension/attributesecurityfilter/AttributeSecurityFilterConfig.java @@ -17,7 +17,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -@ComponentScan("com.esri.arcgis.soi.attributesecurityfilter") +@ComponentScan("com.esri.serverextension.attributesecurityfilter") public class AttributeSecurityFilterConfig { public AttributeSecurityFilterConfig() { diff --git a/examples/clustering-soe/client/index.html b/examples/clustering-soe/client/index.html new file mode 100644 index 0000000..4533c83 --- /dev/null +++ b/examples/clustering-soe/client/index.html @@ -0,0 +1,200 @@ + + + + + + Serverside Clustering + + + + + + + + + + + + + + + + + +
+
+ +
+ + + +
+
+ + +
+ + + + + + +
+ +
+ + + + + + +
+ + + diff --git a/examples/clustering-soe/client/js/layer/ClusterQuery.js b/examples/clustering-soe/client/js/layer/ClusterQuery.js new file mode 100644 index 0000000..7acb251 --- /dev/null +++ b/examples/clustering-soe/client/js/layer/ClusterQuery.js @@ -0,0 +1,28 @@ +define([ + "dojo/_base/declare", + "esri/tasks/query" + + +], function(declare, Query){ + return declare("ClusterQuery", [Query], { + constructor: function(map){ + this.map = map; + this.clusterDistanceInPixels = 100; + this.clusterField = null; + }, + + toJson:function(){ + var obj = this.inherited(arguments); + obj.bbox = JSON.stringify(this.map.extent); + obj.mapUnitsPerPixel = this.map.extent.getWidth()/this.map.width; + obj.clusterDistanceInPixels = this.clusterDistanceInPixels; + obj.clusterField = this.clusterField; + + return obj; + } + + }); + + + +}); \ No newline at end of file diff --git a/examples/clustering-soe/client/js/layer/ServerClusterLayer.js b/examples/clustering-soe/client/js/layer/ServerClusterLayer.js new file mode 100644 index 0000000..f8071c0 --- /dev/null +++ b/examples/clustering-soe/client/js/layer/ServerClusterLayer.js @@ -0,0 +1,359 @@ +define([ + "dojo/_base/declare", + "dojo/_base/array", + "dojo/number", + "esri/Color", + "dojo/_base/connect", + "esri/renderers/ClassBreaksRenderer", + + "esri/SpatialReference", + "esri/geometry/Point", + "esri/graphic", + "esri/symbols/SimpleMarkerSymbol", + "esri/symbols/TextSymbol", + "esri/symbols/SimpleLineSymbol", + "esri/symbols/Font", + + "esri/dijit/PopupTemplate", + "esri/layers/GraphicsLayer", + "dojo/_base/lang", + "esri/tasks/QueryTask", + + "layer/ClusterQuery" + +], function ( + declare, arrayUtils, Number, Color, connect, ClassBreaksRenderer, + SpatialReference, Point, Graphic, SimpleMarkerSymbol, TextSymbol, SimpleLineSymbol, Font, + PopupTemplate, GraphicsLayer, lang, QueryTask, ClusterQuery +) { + return declare([GraphicsLayer], { + constructor: function(options) { + this.url = options.url; + }, + + // override esri/layers/GraphicsLayer methods + _setMap: function(map, surface) { + this._saveMap = map; + + map.on("extent-change", lang.hitch(this, this.changeExtent)); + this.refreshLayer(); + // GraphicsLayer will add its own listener here + var div = this.inherited(arguments); + return div; + }, + + _unsetMap: function() { + this.inherited(arguments); + connect.disconnect(this._zoomEnd); + }, + + changeExtent:function(event){ + this.refreshLayer(); + }, + + stringifyAllBreaks:function(breaks){ + var sBreaks = []; + for (var i=0;i= 10000000000000000) { //10 quadrillion + value = Number.format(value1 / 1000000000000000, { + places: additionalPlaces + 0 + }) + "Q"; //1 quadrillion + } else if (value >= 1000000000000000) { //1 quadrillion + value = Number.format(value1 / 1000000000000000, { + places: additionalPlaces + 0 + }) + "Q"; //1 quadrillion + } else if (value >= 10000000000000) { //10 trillion + value = Number.format(value1 / 1000000000000, { + places: additionalPlaces + 0 + }) + "T"; //1 trillion + } else if (value >= 1050000000000) { //1 trillion + value = Number.format(value1 / 1000000000000, { + places: additionalPlaces + 1 + }) + "T"; //1 trillion + } else if (value >= 1000000000000) { //1 trillion + value = Number.format(value1 / 1000000000000, { + places: additionalPlaces + 0 + }) + "T"; //1 trillion + } else if (value >= 10000000000) { //10 billion + value = Number.format(value1 / 1000000000, { + places: additionalPlaces + 0 + }) + "B"; + } else if (value >= 1050000000) { //1.05 billion + value = Number.format(value1 / 1000000000, { + places: additionalPlaces + 1 + }) + "B"; + } else if (value >= 1000000000) { //1 billion + value = Number.format(value1 / 1000000000, { + places: additionalPlaces + 0 + }) + "B"; + } else if (value >= 10000000) { //10 million + value = Number.format(value1 / 1000000, { + places: additionalPlaces + 0 + }) + "M"; + } else if (value >= 1050000) { //1.05 million + value = Number.format(value1 / 1000000, { + places: additionalPlaces + 1 + }) + "M"; + } else if (value >= 1000000) { //1 million + value = Number.format(value1 / 1000000, { + places: additionalPlaces + 0 + }) + "M"; + } else if (value >= 10000) { // 10K + value = Number.format(value1 / 1000, { + places: additionalPlaces + 0 + }) + "K"; + } else if (value >= 1050) { + value = Number.format(value1 / 1000, { + places: additionalPlaces + 1 + }) + "K"; + } else if (value >= 1000) { + value = Number.format(value1 / 1000, { + places: additionalPlaces + 0 + }) + "K"; + } else if (value >= 1 ) { + value = Number.format(value1, { + places: Math.min(additionalPlaces + 0, numPlaces) + }); + } else if (value >= 0.1) { + value = Number.format(value1, { + places: Math.min(additionalPlaces + 1, numPlaces) + }); + } else if (value >= 0.01) { + value = Number.format(value1, { + places: Math.min(additionalPlaces + 2, numPlaces) + }); + } else if (value >= 0.001) { + value = Number.format(value1, { + places: Math.min(additionalPlaces + 3, numPlaces) + }); + } else if (value >= 0.0001) { + value = Number.format(value1, { + places: Math.min(additionalPlaces + 4, numPlaces) + }); + } else if (value >= 0.00001) { + value = Number.format(value1, { + places: Math.min(additionalPlaces + 5, numPlaces) + }); + } else if (value >= 0.000001) { + value = Number.format(value1, { + places: Math.min(additionalPlaces + 6, numPlaces) + }); + } else { + value = Number.format(value1); + } + + } + } + return value; + }, + + + + getAllInfos:function(breaks){ + var newBreaks = this.stringifyAllBreaks(breaks); + var infoArray = []; + for (var i=0;i 5){ + var inc = Math.floor(features.length/5); + for (var j=0;j<4;j++){ + var feature = features[inc*j]; + var val = feature.attributes[fieldName]; + breaks.push(val); + /* + if (j<4){ + feature = features[inc*(j+1)]; + val = feature.attributes[fieldName]; + breaks.push(val); + } + */ + } + + breaks.push(features[features.length-1].attributes[fieldName]); + + }else if (features.length > 0){ + breaks.push(features[0].attributes[fieldName]); + breaks.push(features[features.length-1].attributes[fieldName]); + }else{ + breaks.push(0); + breaks.push(1); + } + + + + var white = new Color([255,255, 255, 0.7]); + var ringColor = new Color([0, 163, 98, 0.7]); + var symbols = []; + symbols[0] = new SimpleMarkerSymbol(SimpleMarkerSymbol.STYLE_CIRCLE, 28, + new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, ringColor, 3), + white); + symbols[1] = new SimpleMarkerSymbol(SimpleMarkerSymbol.STYLE_CIRCLE, 34, + new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, ringColor, 5), + white); + symbols[2] = new SimpleMarkerSymbol(SimpleMarkerSymbol.STYLE_CIRCLE, 40, + new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, ringColor, 8), + white); + symbols[3] = new SimpleMarkerSymbol(SimpleMarkerSymbol.STYLE_CIRCLE, 46, + new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, ringColor, 10), + white); + symbols[4] = new SimpleMarkerSymbol(SimpleMarkerSymbol.STYLE_CIRCLE, 52, + new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, ringColor, 12), + white); + + var renderer = new ClassBreaksRenderer(null, fieldName); + + var infos = this.getAllInfos(breaks); + + for (var k=0;k + + + 4.0.0 + + com.esri.serverextensions + server-extension-java + 0.2.0 + ../../pom.xml + + clustering-soe + clustering-soe + A Server Object Extension (SOE) example for clustering point data server-side. + jar + + + com.esri.serverextensions + server-extension-core + ${project.parent.version} + + + + + + + src/main/resources + true + + **/*.properties + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + src/main/assemblies + + + + true + + + + + + org.codehaus.gmaven + groovy-maven-plugin + 2.0 + + + org.safehaus.jug + jug + 2.0.0 + asl + + + + + package + + execute + + + + import org.safehaus.uuid.UUIDGenerator + import java.util.Date + def uuid = UUIDGenerator.getInstance().generateRandomBasedUUID() + project.properties.setProperty('soe.uuid', uuid.toString()) + def date = new Date() + project.properties.setProperty('soe.timestamp', date.format('EEE MMM d k:mm:ss z yyyy')) + project.properties.setProperty('soe.hhmm', date.format('HHmm')) + + + + + + + maven-assembly-plugin + 3.0.0 + + + package + + single + + + true + + src/assembly/soe-assembly.xml + + + + + + + maven-antrun-plugin + 1.8 + + + rename-to-dot-soe + install + + + + + + + run + + + + + + pl.project13.maven + git-commit-id-plugin + 2.2.2 + + + + revision + + + + + ${project.basedir}/../../.git + true + + + + + \ No newline at end of file diff --git a/examples/clustering-soe/src/assembly/soe-assembly.xml b/examples/clustering-soe/src/assembly/soe-assembly.xml new file mode 100644 index 0000000..434b784 --- /dev/null +++ b/examples/clustering-soe/src/assembly/soe-assembly.xml @@ -0,0 +1,40 @@ + + + + soe-assembly + + zip + + false + + + Install + true + false + runtime + + + + + src/assembly/soe-config.xml + + true + Config.xml + + + \ No newline at end of file diff --git a/examples/clustering-soe/src/assembly/soe-config.xml b/examples/clustering-soe/src/assembly/soe-config.xml new file mode 100644 index 0000000..e9942cc --- /dev/null +++ b/examples/clustering-soe/src/assembly/soe-config.xml @@ -0,0 +1,51 @@ + + + + ${project.name} + ${project.description} + ${soe.timestamp} + + + + ${project.version} + + + + {${soe.uuid}} + + + MapServer + + + clustering + Clustering + ${project.description} + + + + + false + true + false + false + + + + + + + + + diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/AboutResource.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/AboutResource.java new file mode 100644 index 0000000..880ec16 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/AboutResource.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.arcgis.server.json.JSONObject; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.annotation.Resource; + +@Service +public class AboutResource { + + private String projectName; + private String projectDescription; + private String projectVersion; + private String gitBranch; + private String gitCommitID; + + public AboutResource() { + } + + @Resource(name = "projectName") + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + @Resource(name = "projectDescription") + public void setProjectDescription(String projectDescription) { + this.projectDescription = projectDescription; + } + + @Resource(name = "projectVersion") + public void setProjectVersion(String projectVersion) { + this.projectVersion = projectVersion; + } + + @Resource(name = "gitBranch") + public void setGitBranch(String gitBranch) { + this.gitBranch = gitBranch; + } + + @Resource(name = "gitCommitID") + public void setGitCommitID(String gitCommitID) { + this.gitCommitID = gitCommitID; + } + + @RequestMapping("/about") + public JSONObject getAboutResource() { + JSONObject aboutResource = new JSONObject(); + aboutResource.put("name", projectName); + aboutResource.put("description", projectDescription); + aboutResource.put("version", projectVersion); + aboutResource.put("gitBranch", gitBranch); + aboutResource.put("gitCommitID", gitCommitID); + return aboutResource; + } + +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/Cluster.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/Cluster.java new file mode 100644 index 0000000..dfd5fd6 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/Cluster.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import java.util.ArrayList; + +/** + * Created by kcoffin on 2/8/17. + */ +public class Cluster { + + private double _clusterCount; + private ClusterPoint _point; + private ArrayList _features; + + //A cluster needs to be created with at least one feature + public Cluster(ClusterFeature feature){ + _features = new ArrayList<>(); + _point = new ClusterPoint(feature.getPoint()); + _clusterCount = feature.getValue(); + _features.add(feature); + } + + public ClusterPoint getPoint(){ + return _point; + } + + public void setClusterCount(double ct){ + _clusterCount = ct; + } + + public void addFeature(ClusterFeature feature){ + double value = feature.getValue(); + double count = _clusterCount; + _features.add(feature); + double ptc = value/(count + value); + double ctc = count/(count + value); + ClusterPoint cluster = _point; + ClusterPoint p = feature.getPoint(); + + double x = (p.x * ptc + (cluster.x * ctc)); + double y = (p.y * ptc + (cluster.y * ctc)); + cluster.x = x; + cluster.y = y; + _clusterCount += value; + } + + public void addPointCluster(ClusterFeature feature, double ptCount){ + double count, x, y; + count = getValue(); + + ClusterPoint p = feature.getPoint(); + getFeatures().add(feature); + + double ptc = ptCount/(count + ptCount); + double ctc = count/(count + ptCount); + + x = (p.x * ptc + (_point.x * ctc)); + y = (p.y * ptc + (_point.y * ctc)); + _clusterCount += ptCount; + _point.x = x; + _point.y = y; + } + + public double getValue(){ + return _clusterCount; + } + + public ArrayList getFeatures(){ + return _features; + } + + + + + + public void print(){ + System.out.print("Cluster value:"+_clusterCount+" "); + _point.print(); + System.out.println(); + for(ClusterFeature f:_features){ + f.print(); + } + System.out.println("=============="); + } + +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterAssembler.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterAssembler.java new file mode 100644 index 0000000..d00d6a8 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterAssembler.java @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import java.util.*; + +/** + * Assembles the clustering + */ +public class ClusterAssembler { + + //The grid's extent + private ClusterExtent _extent; + + + //cellsize of grid + private double _cellSize; + + private double _mapUnitsPerPixel; + + //number of columns in grid + private int _numColumns; + + //number of columns in grid + private int _numRows; + + + //the cells which keeps the clustering info + private Map> _cells; + + //the desired cluster distance in pixels + private double _clusterDistanceInPixels; + + //All the clusters which are created + private ArrayList _clusters; + + + /** + * Assemble the clustering + * @param features all the features + * @param mapUnitsPerPixel map units per pixel (like meters per pixel) + * @param clusterDistanceInPixels cluster distance in pixels + * @param extent the extent in real world coordinates + */ + public ClusterAssembler(ArrayList features, double mapUnitsPerPixel, + double clusterDistanceInPixels, ClusterExtent extent){ + addAllFeatures(features, mapUnitsPerPixel, clusterDistanceInPixels, extent); + } + + /** + * Retrieves all the clusters + * @return + */ + public ArrayListgetClusters(){ + return _clusters; + } + + + //Adds all the features. Called internally by the ctor + private void addAllFeatures(ArrayList features, double mapUnitsPerPixel, + double clusterDistanceInPixels, ClusterExtent extent + ){ + _cells = new HashMap<> (); + _mapUnitsPerPixel = mapUnitsPerPixel; + _clusterDistanceInPixels = clusterDistanceInPixels; + _cellSize = mapUnitsPerPixel * _clusterDistanceInPixels; + _extent = extent; + _numColumns = getGridColumn(extent.getXMax())+1; + _numRows = getGridRow(extent.getYMax())+1; + _clusters = new ArrayList<>(); + + //first sort the features based on the clusterfieldIndex + // Sorting by Lambda + Collections.sort(features, (ClusterFeature feature2, ClusterFeature feature1)-> + ((Double)feature1.getValue()).compareTo(feature2.getValue())); + + + /* JAVA 1.7 + Collections.sort(features, new Comparator() { + @Override + public int compare(ClusterFeature feature2, ClusterFeature feature1) + { + return ((Double)feature1.getValue()).compareTo((Double)feature2.getValue()); + } + }); + */ + + for (ClusterFeature feature:features){ + addFeature(feature); + } + + + + for (Cluster cluster:_clusters){ + fixCluster(cluster); + } + + + for (Cluster cluster:_clusters){ + fixCluster(cluster); + } + +/* + System.out.println("+++++++++++++++++++++++++++++++++++++"); + for (Cluster cluster:_clusters){ + examineCluster(cluster); + } +*/ +/* + for (Cluster cluster:_clusters){ + cluster.print(); + } +*/ + + } + + + + + /** + * Add a feature + * @param feature + */ + private void addFeature(ClusterFeature feature) { + Cluster closestCluster = getClosestCluster(feature.getPoint()); + + if (closestCluster != null) { + addFeatureToCluster(feature, closestCluster); + }else{ + createCluster(feature);//create new cluster + } + + } + + //from yValue real-world, what is the grid row + private int getGridRow(double yValue){ + return (int) Math.floor((yValue-_extent.getYMin())/_cellSize); + } + + //from xValue real-world, what is the grid column + private int getGridColumn(double xValue){ + return (int) Math.floor((xValue-_extent.getXMin())/_cellSize); + } + + //add a feature to an EXISTING cluster + private void addFeatureToCluster(ClusterFeature feature, Cluster cluster) { + //remove it from the grid because its coordinates are going to change + removeClusterFromGrid(cluster); + + //add the feature to the cluster + cluster.addFeature(feature); + + //add it back in to the grid + addClusterToGrid(cluster); + } + + //remove a cluster from the grid (it is already in the grid) + private void removeClusterFromGrid(Cluster cluster){ + ClusterPoint pt = cluster.getPoint(); + int row = getGridRow(pt.y); + int column = getGridColumn(pt.x); + int index = _numRows * row + column; + ArrayList cell = _cells.get(index); + + if (cell != null) { + cell.remove(cluster); + }else{ + System.out.println("Programming error"); + } + } + + //Add the cluster to the grid + private void addClusterToGrid(Cluster cluster){ + ClusterPoint pt = cluster.getPoint(); + int row = getGridRow(pt.y); + int column = getGridColumn(pt.x); + int index = _numRows * row + column; + ArrayList cell = _cells.get(index); + + if (cell == null) { + cell = new ArrayList<>(); + _cells.put(index, cell); + } + cell.add(cluster); + } + + private void createCluster(ClusterFeature feature) { + Cluster cluster = new Cluster(feature); + addClusterToGrid(cluster); + _clusters.add(cluster); + } + + + + + /** + * Gets the closest cluster within the cell distance + * @param pt + * @return + */ + public Cluster getClosestCluster(ClusterPoint pt){ + int row = getGridRow(pt.y); + int column = getGridColumn(pt.x); + + //should never happen as all features should come from within the extent + if (row < 0 || column < 0 || row>=_numRows || column>=_numColumns){ + System.out.println("There's an error in the query"); + return null; + } + + int yStart = row; + int yEnd = row; + if (row > 0){ + yStart = row-1; + } + if (row < _numRows-1){ + yEnd = row+1; + } + + + int xStart = column; + int xEnd = column; + if (column > 0){ + xStart = column-1; + } + if (column < _numColumns-1){ + xEnd = column+1; + } + + /* + int xStart = (int)(Math.floor((extent.xmin - this._xmin) / this._cellSize)); + int xEnd = (int)(Math.floor((extent.xmax - this._xmin) / this._cellSize)); + int yStart = (int)(Math.floor((extent.ymin - this._ymin) / this._cellSize)); + int yEnd = (int)(Math.floor((extent.ymax - this._ymin) / this._cellSize)); + */ + + Cluster minCluster = null; + double minDis2 = Double.MAX_VALUE; + + for (int x = xStart; x <= xEnd; x++) { + for (int y = yStart; y <= yEnd; y++) { + int index = _numRows * y + x; + ArrayList cell = _cells.get(index); + if (cell != null) { + for (int i = 0; i < cell.size(); i++) { + Cluster cluster = cell.get(i); + double dis2 = pt.squareDistance(cluster.getPoint()); + if (dis2 < minDis2) { + minDis2 = dis2; + minCluster = cluster; + } + } + } + } + } + if (minDis2 > 0){ + minDis2 = Math.sqrt(minDis2); + } + if (minDis2 > _cellSize){ + return null; + } + return minCluster; + } + + + + + //This examines a cluster and determines if all features in it are the closest to it. + public void examineCluster(Cluster cluster){ + + + ArrayList features = cluster.getFeatures(); + for (int k=0;k clusterFeatures = new ArrayList<>(); + + public ClusterAssemblerCallbackHandler(String clusterFieldName) { + this.clusterFieldName = clusterFieldName; + } + + public int getFeatureCount() { + return featureCount; + } + + public ArrayList getClusterFeatures() { + return clusterFeatures; + } + + @Override + public void setGeodatabaseFieldMap(GeodatabaseFieldMap fieldMap) throws IOException { + this.fieldMap = fieldMap; + clusterFieldIndex = fieldMap.get(clusterFieldName).getIndex(); + } + + @Override + public void processRow(IRow row) throws IOException { + throw new UnsupportedOperationException("This callback handler only supports features."); + } + + @Override + public void processFeature(IFeature feature) throws IOException { + featureCount++; + Map attributes = new LinkedHashMap<>(); + for (GeodatabaseFieldMap.FieldIndex fieldIndex : fieldMap.getFieldIndices()) { + attributes.put(fieldIndex.getField().getName(), feature.getValue(fieldIndex.getIndex())); + } + IGeometry geometry = feature.getShape(); + if (geometry instanceof IPoint && !geometry.isEmpty()) { + IPoint point = (IPoint)geometry; + ClusterPoint clusterPoint = new ClusterPoint(point.getX(), point.getY()); + Object value = feature.getValue(clusterFieldIndex); + if (value == null) { + return; + } + if (value instanceof Number) { + ClusterFeature clusterFeature = new ClusterFeature(clusterPoint, + ((Number) value).doubleValue()); + clusterFeatures.add(clusterFeature); + } + } + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterExtent.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterExtent.java new file mode 100644 index 0000000..a4ba376 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterExtent.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +/** + * Immutable ClusterExtent + */ +public class ClusterExtent { + private double _xmin; + private double _ymin; + private double _xmax; + private double _ymax; + + /** + * Construct an extent. This is where everything gets assigned + * @param xmin + * @param ymin + * @param xmax + * @param ymax + */ + public ClusterExtent(double xmin, double ymin, double xmax, double ymax){ + _xmin = xmin; + _ymin = ymin; + _xmax = xmax; + _ymax = ymax; + } + + public double getWidth(){ + return _xmax - _xmin; + } + public double getHeight(){ + return _ymax - _ymin; + } + public double getXMin(){ + return _xmin; + } + public double getYMin(){ + return _ymin; + } + public double getXMax(){ + return _xmax; + } + public double getYMax(){ + return _ymax; + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterFeature.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterFeature.java new file mode 100644 index 0000000..d3ff6e3 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterFeature.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +/** + * Created by kcoffin on 2/8/17. + */ +public class ClusterFeature { + + private double _value; + private ClusterPoint _point; + + public ClusterFeature(ClusterPoint point, double value){ + _point = point; + _value = value; + } + + public double getValue(){ + return _value; + } + + + public ClusterPoint getPoint(){ + return _point; + } + + public void print(){ + System.out.print("ClusterFeature value:"+_value+" "); + _point.print(); + System.out.println(); + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterLayerResource.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterLayerResource.java new file mode 100644 index 0000000..5e09085 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterLayerResource.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.arcgis.carto.IMapLayerInfo; +import com.esri.arcgis.geodatabase.*; +import com.esri.arcgis.geometry.IPoint; +import com.esri.arcgis.geometry.ISpatialReference; +import com.esri.arcgis.geometry.Point; +import com.esri.arcgis.interop.Cleaner; +import com.esri.arcgis.server.json.JSONObject; +import com.esri.serverextension.core.geodatabase.GeodatabaseTemplate; +import com.esri.serverextension.core.rest.api.*; +import com.esri.serverextension.core.rest.api.Feature; +import com.esri.serverextension.core.rest.api.Field; +import com.esri.serverextension.core.rest.api.FieldType; +import com.esri.serverextension.core.server.ServerObjectExtensionContext; +import com.esri.serverextension.core.util.ArcObjectsInteropException; +import com.esri.serverextension.core.util.GenericEsriEnum; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.ws.rs.BeanParam; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ClusterLayerResource { + + @RequestMapping("/layers/{layerId}") + public JSONObject getLayerResource(@PathVariable("layerId") int layerId, ServerObjectExtensionContext serverContext) { + IMapLayerInfo layerInfo = MapServerUtilities.getPointFeatureLayerByID(layerId, serverContext); + JSONObject layerObject = new JSONObject(); + try { + layerObject.put("name", layerInfo.getName()); + layerObject.put("id", layerInfo.getID()); + layerObject.put("description", layerInfo.getDescription()); + } catch (IOException ex) { + throw new ArcObjectsInteropException( + String.format("Failed to get details for layer: %1$d", layerId)); + } + return layerObject; + } + + @RequestMapping("/layers/{layerId}/query") + public FeatureSet query(@PathVariable("layerId") int layerId, @BeanParam ClusterQueryOperationInput input, ServerObjectExtensionContext serverContext) { + try { + IFeatureClass featureClass = MapServerUtilities.getPointFeatureClassByLayerID(layerId, serverContext); + IQueryFilter queryFilter = getQueryFilter(input, featureClass.getShapeFieldName()); + ClusterAssemblerCallbackHandler clusterAssemblerCallbackHandler = new ClusterAssemblerCallbackHandler(input.getClusterField()); + GeodatabaseTemplate geodatabaseTemplate = new GeodatabaseTemplate(); + geodatabaseTemplate.query(featureClass, queryFilter, clusterAssemblerCallbackHandler); + ClusterExtent clusterExtent = new ClusterExtent( + input.getBbox().getXmin(), + input.getBbox().getYmin(), + input.getBbox().getXmax(), + input.getBbox().getYmax() + ); + ClusterAssembler clusterAssembler = new ClusterAssembler( + clusterAssemblerCallbackHandler.getClusterFeatures(), + input.getMapUnitsPerPixel(), + input.getClusterDistanceInPixels(), + clusterExtent + ); + + FeatureSet featureSet = new FeatureSet(); + featureSet.setDisplayFieldName(input.getClusterField()); + Field field = new Field(input.getClusterField(), + FieldType.esriFieldTypeDouble, input.getClusterField()); + List fields = new ArrayList<>(); + fields.add(field); + featureSet.setFields(fields); + featureSet.setSpatialReference(getOutSpatialReference(input, serverContext)); + featureSet.setGeometryType(GeometryType.esriGeometryPoint); + List clusters = clusterAssembler.getClusters(); + if (!CollectionUtils.isEmpty(clusters)) { + List features = new ArrayList<>(clusterAssembler.getClusters().size()); + for (Cluster cluster : clusterAssembler.getClusters()) { + if (cluster.getValue() == 0.0d) { + continue; + } + Feature clusterFeature = new Feature(); + ClusterPoint clusterPoint = cluster.getPoint(); + IPoint point = new Point(); + point.setX(clusterPoint.x); + point.setY(clusterPoint.y); + clusterFeature.setGeometry(point); + Map attributes = new LinkedHashMap<>(); + attributes.put(input.getClusterField().intern(), cluster.getValue()); + clusterFeature.setAttributes(attributes); + features.add(clusterFeature); + } + featureSet.setFeatures(features); + } + return featureSet; + } catch (IOException ex) { + throw new ArcObjectsInteropException( + String.format("Failed to query cluster layer: %1$d", layerId)); + } + } + + private IQueryFilter getQueryFilter(ClusterQueryOperationInput input, String shapeFieldName) { + try { + IQueryFilter2 queryFilter = null; + if (input.getGeometry() != null) { + SpatialFilter spatialFilter = new SpatialFilter(); + spatialFilter.setGeometryByRef(input.getGeometry()); + spatialFilter.setGeometryField(shapeFieldName); + if (input.getSpatialRel() != null) { + spatialFilter.setSpatialRel(GenericEsriEnum.valueOf(esriSpatialRelEnum.class, input.getSpatialRel().name())); + } + if (StringUtils.isNotEmpty(input.getRelationParam())) { + spatialFilter.setSpatialRelDescription(input.getRelationParam()); + } + if (input.getOutSR() != null) { + spatialFilter.setOutputSpatialReferenceByRef(shapeFieldName, input.getOutSR()); + } + queryFilter = spatialFilter; + } else { + queryFilter = new QueryFilter(); + } + if (StringUtils.isNotEmpty(input.getWhere())) { + SQLCheck sqlCheck = new SQLCheck(); + sqlCheck.checkWhereClause(input.getWhere()); + Cleaner.release(sqlCheck); + queryFilter.setWhereClause(input.getWhere()); + } + if (StringUtils.isNotEmpty(input.getOrderByFields())) { + ((IQueryFilterDefinition)queryFilter).setPostfixClause(String.format( + "ORDER BY %1$s", input.getOrderByFields() + )); + } + queryFilter.setSpatialResolution(100000.0d); + if (StringUtils.isNotEmpty(input.getClusterField())) { + queryFilter.setSubFields(input.getClusterField()); + queryFilter.addField(shapeFieldName); + } + return queryFilter; + } catch (IOException ex) { + throw new ArcObjectsInteropException("Failed to create query filter.", ex); + } + } + + private ISpatialReference getOutSpatialReference(ClusterQueryOperationInput input, ServerObjectExtensionContext serverContext) { + if (input.getOutSR() != null) { + return input.getOutSR(); + } + return MapServerUtilities.getMapSpatialReference(serverContext); + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterLayersResource.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterLayersResource.java new file mode 100644 index 0000000..98b3264 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterLayersResource.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.arcgis.carto.IMapLayerInfo; +import com.esri.arcgis.server.json.JSONArray; +import com.esri.arcgis.server.json.JSONObject; +import com.esri.serverextension.core.server.ServerObjectExtensionContext; +import com.esri.serverextension.core.util.ArcObjectsInteropException; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.io.IOException; +import java.util.List; + +@Service +public class ClusterLayersResource { + + @RequestMapping("/layers") + public JSONObject getLayersResource(ServerObjectExtensionContext serverContext) { + JSONArray layersArray = new JSONArray(); + List pointFeatureLayers = MapServerUtilities.getPointFeatureLayers(serverContext); + for (IMapLayerInfo layerInfo : pointFeatureLayers) { + try { + JSONObject layer = new JSONObject(); + layer.put("name", layerInfo.getName()); + layer.put("id", layerInfo.getID()); + layer.put("description", layerInfo.getDescription()); + layersArray.put(layer); + } catch (IOException ex) { + throw new ArcObjectsInteropException( + "Failed to get details from map layer info."); + } + } + JSONObject response = new JSONObject(); + response.put("layers", layersArray); + return response; + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterPoint.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterPoint.java new file mode 100644 index 0000000..0f59bd9 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterPoint.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +/** + * Created by kcoffin on 2/8/17. + */ +public class ClusterPoint { + public double x; + public double y; + public ClusterPoint(double x, double y){ + this.x = x; + this.y = y; + } + public ClusterPoint(ClusterPoint pt){ + x = pt.x; + y = pt.y; + } + + public double squareDistance(ClusterPoint pt){ + double dx = pt.x - x; + double dy = pt.y - y; + return (dx*dx) + (dy*dy); + } + public double distance(ClusterPoint pt){ + return Math.sqrt(squareDistance(pt)); + } + + + + public void print(){ + System.out.print("("+x+","+y+")"); + + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterQueryOperationInput.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterQueryOperationInput.java new file mode 100644 index 0000000..84c0f8f --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusterQueryOperationInput.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.serverextension.core.rest.api.Extent; +import com.esri.serverextension.core.rest.api.QueryMapServiceLayerOperationInput; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class ClusterQueryOperationInput extends QueryMapServiceLayerOperationInput { + + private static final long serialVersionUID = 1L; + + private Extent bbox; + private Double mapUnitsPerPixel; + private Integer clusterDistanceInPixels; + private String clusterField; + + public ClusterQueryOperationInput() { + } + + public Extent getBbox() { + return bbox; + } + + public void setBbox(Extent bbox) { + this.bbox = bbox; + } + + public Double getMapUnitsPerPixel() { + return mapUnitsPerPixel; + } + + public void setMapUnitsPerPixel(Double mapUnitsPerPixel) { + this.mapUnitsPerPixel = mapUnitsPerPixel; + } + + public Integer getClusterDistanceInPixels() { + return clusterDistanceInPixels; + } + + public void setClusterDistanceInPixels(Integer clusterDistanceInPixels) { + this.clusterDistanceInPixels = clusterDistanceInPixels; + } + + public String getClusterField() { + return clusterField; + } + + public void setClusterField(String clusterField) { + this.clusterField = clusterField; + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusteringConfig.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusteringConfig.java new file mode 100644 index 0000000..1b20713 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusteringConfig.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.env.Environment; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Configuration +@ComponentScan("com.esri.serverextension.cluster") +@PropertySource(value = { "classpath:/buildInfo.properties", "classpath:/git.properties" }) +public class ClusteringConfig { + + @Value("${project.name}") + public String projectName; + @Value("${project.description}") + public String projectDescription; + @Value("${project.version}") + public String projectVersion; + @Value("${git.branch}") + public String gitBranch; + @Value("${git.commit.id}") + public String gitCommitID; + @Inject + private Environment env; + + public ClusteringConfig() { + } + + @Bean + public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Bean + @Singleton + public String projectName() { + return projectName; + } + + @Bean + @Singleton + public String projectDescription() { + return projectDescription; + } + + @Bean + @Singleton + public String projectVersion() { + return projectVersion; + } + + @Bean + @Singleton + public String gitBranch() { + return gitBranch; + } + + @Bean + @Singleton + public String gitCommitID() { + return gitCommitID; + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusteringExtension.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusteringExtension.java new file mode 100644 index 0000000..6640dba --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/ClusteringExtension.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.arcgis.interop.AutomationException; +import com.esri.arcgis.interop.extn.ArcGISExtension; +import com.esri.arcgis.interop.extn.ServerObjectExtProperties; +import com.esri.arcgis.server.json.JSONArray; +import com.esri.arcgis.server.json.JSONObject; +import com.esri.arcgis.system.ServerUtilities; +import com.esri.serverextension.core.server.AbstractRestServerObjectExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import java.io.IOException; + +@ArcGISExtension +@ServerObjectExtProperties(displayName = "Clustering", + description = "Provides server-side clustering of point features.", + interceptor = true, + servicetype = "MapService") +public class ClusteringExtension extends AbstractRestServerObjectExtension { + + public static final String QUERY_OPERATION_PARAMETER_NAMES = "bbox, " + + "mapUnitsPerPixel, clusterDistanceInPixels, clusterField, " + + "geometry, geometryType, inSR, spatialRel, relationParam, where, " + + "outField, outSR, orderByFields"; + + @Override + protected void doConfigure( + AnnotationConfigApplicationContext applicationContext) { + super.doConfigure(applicationContext); + applicationContext.register(ClusteringConfig.class); + } + + @Override + protected void doShutdown() { + super.doShutdown(); + } + + @Override + public String getSchema() throws IOException, AutomationException { + ServerObjectExtProperties annotation = this.getClass().getAnnotation( + ServerObjectExtProperties.class); + JSONObject rootResource = ServerUtilities.createResource( + annotation.displayName(), annotation.description(), false, + false); + + JSONArray resources = new JSONArray(); + rootResource.put("resources", resources); + + JSONArray layerResourceOperations = new JSONArray(); + + JSONObject layerQueryOperation = ServerUtilities.createOperation( + "query", QUERY_OPERATION_PARAMETER_NAMES, "json", false); + layerResourceOperations.put(layerQueryOperation); + + JSONObject layersResource = ServerUtilities.createResource("layers", + "Cluster layers", true, true); + layersResource.put("operations", layerResourceOperations); + resources.put(layersResource); + + JSONObject about = ServerUtilities.createResource("about", + "About Clustering", false, false); + resources.put(about); + + return rootResource.toString(); + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/MapServerUtilities.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/MapServerUtilities.java new file mode 100644 index 0000000..0a7cad0 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/MapServerUtilities.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.arcgis.carto.*; +import com.esri.arcgis.geodatabase.FeatureClass; +import com.esri.arcgis.geodatabase.IFeatureClass; +import com.esri.arcgis.geometry.ISpatialReference; +import com.esri.arcgis.geometry.esriGeometryType; +import com.esri.arcgis.interop.Cleaner; +import com.esri.serverextension.core.server.ServerObjectExtensionContext; +import com.esri.serverextension.core.util.ArcObjectsInteropException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class MapServerUtilities { + + public static final List getPointFeatureLayers( + ServerObjectExtensionContext serverContext) { + try { + List layerInfoList = new ArrayList<>(); + IMapServerDataAccess mapServerDataAccess = (IMapServerDataAccess) serverContext + .getServerObject(); + IMapServer3 mapServer = (IMapServer3) mapServerDataAccess; + String mapName = mapServer.getDefaultMapName(); + IMapServerInfo4 mapServerInfo = (IMapServerInfo4) mapServer + .getServerInfo(mapName); + IMapLayerInfos layerInfos = mapServerInfo.getMapLayerInfos(); + int layerCount = layerInfos.getCount(); + for (int i = 0; i < layerCount; i++) { + IMapLayerInfo layerInfo = layerInfos.getElement(i); + if (layerInfo.isComposite()) { + continue; + } + if (layerInfo.isFeatureLayer()) { + Object dataSource = mapServerDataAccess + .getDisplayDataSource(mapName, layerInfo.getID()); + IFeatureClass featureClass = new FeatureClass(dataSource); + int geometryType = featureClass.getShapeType(); + if (geometryType == esriGeometryType.esriGeometryPoint) { + layerInfoList.add(layerInfo); + } + Cleaner.release(featureClass); + } + } + return layerInfoList; + } catch (IOException ex) { + throw new ArcObjectsInteropException( + "Failed to get point feature layers from map server object."); + } + } + + public static final IMapLayerInfo getPointFeatureLayerByID(int id, + ServerObjectExtensionContext serverContext) { + List layerInfoList = getPointFeatureLayers(serverContext); + for (IMapLayerInfo layerInfo : layerInfoList) { + try { + if (layerInfo.getID() == id) { + return layerInfo; + } + } catch (IOException ex) { + throw new ArcObjectsInteropException( + String.format("Failed to access point feature layer: %1$d", id)); + } + } + throw new IllegalArgumentException(String.format("No such point feature layer: %1$d", id)); + } + + public static final IFeatureClass getPointFeatureClassByLayerID(int layerId, + ServerObjectExtensionContext serverContext) { + try { + IMapLayerInfo layerInfo = getPointFeatureLayerByID(layerId, serverContext); + IMapServerDataAccess mapServerDataAccess = (IMapServerDataAccess) serverContext + .getServerObject(); + IMapServer3 mapServer = (IMapServer3) mapServerDataAccess; + String mapName = mapServer.getDefaultMapName(); + Object dataSource = mapServerDataAccess + .getDisplayDataSource(mapName, layerInfo.getID()); + return new FeatureClass(dataSource); + } catch (IOException ex) { + throw new ArcObjectsInteropException( + "Failed to get point feature class from map server object."); + } + } + + public static final ISpatialReference getMapSpatialReference(ServerObjectExtensionContext serverContext) { + try { + IMapServer3 mapServer = (IMapServer3) serverContext + .getServerObject(); + String mapName = mapServer.getDefaultMapName(); + IMapServerInfo mapServerInfo = (IMapServerInfo) mapServer + .getServerInfo(mapName); + IMapDescription mapDescription = mapServerInfo.getDefaultMapDescription(); + return mapDescription.getSpatialReference(); + } catch (IOException ex) { + throw new ArcObjectsInteropException( + "Failed to get point feature class from map server object."); + } + } +} diff --git a/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/RootResource.java b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/RootResource.java new file mode 100644 index 0000000..f540757 --- /dev/null +++ b/examples/clustering-soe/src/main/java/com/esri/serverextension/cluster/RootResource.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.arcgis.carto.IMapLayerInfo; +import com.esri.arcgis.interop.extn.ServerObjectExtProperties; +import com.esri.arcgis.server.json.JSONArray; +import com.esri.arcgis.server.json.JSONObject; +import com.esri.serverextension.core.server.ServerObjectExtensionContext; +import com.esri.serverextension.core.util.ArcObjectsInteropException; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.io.IOException; +import java.util.List; + +@Service +public class RootResource { + + @RequestMapping("/") + public JSONObject getRootResource(ServerObjectExtensionContext serverContext) { + ServerObjectExtProperties annotation = ClusteringExtension.class + .getAnnotation(ServerObjectExtProperties.class); + + JSONObject rootResource = new JSONObject(); + rootResource.put("name", annotation.displayName()); + rootResource.put("description", annotation.description()); + + JSONArray layersArray = new JSONArray(); + List pointFeatureLayers = MapServerUtilities.getPointFeatureLayers(serverContext); + for (IMapLayerInfo layerInfo : pointFeatureLayers) { + try { + JSONObject layer = new JSONObject(); + layer.put("name", layerInfo.getName()); + layer.put("id", layerInfo.getID()); + layer.put("description", layerInfo.getDescription()); + layersArray.put(layer); + } catch (IOException ex) { + throw new ArcObjectsInteropException( + "Failed to get details from map layer info."); + } + } + rootResource.put("layers", layersArray); + return rootResource; + } +} diff --git a/examples/clustering-soe/src/main/resources/buildInfo.properties b/examples/clustering-soe/src/main/resources/buildInfo.properties new file mode 100644 index 0000000..dcf6ef2 --- /dev/null +++ b/examples/clustering-soe/src/main/resources/buildInfo.properties @@ -0,0 +1,3 @@ +project.name=${project.name} +project.description=${project.description} +project.version=${project.version} diff --git a/examples/clustering-soe/src/main/resources/git.properties b/examples/clustering-soe/src/main/resources/git.properties new file mode 100644 index 0000000..d5e4218 --- /dev/null +++ b/examples/clustering-soe/src/main/resources/git.properties @@ -0,0 +1,21 @@ +git.tags=${git.tags} +git.branch=${git.branch} +git.dirty=${git.dirty} +git.remote.origin.url=${git.remote.origin.url} +git.commit.id=${git.commit.id} +git.commit.id.abbrev=${git.commit.id.abbrev} +git.commit.id.describe=${git.commit.id.describe} +git.commit.id.describe-short=${git.commit.id.describe-short} +git.commit.user.name=${git.commit.user.name} +git.commit.user.email=${git.commit.user.email} +git.commit.message.full=${git.commit.message.full} +git.commit.message.short=${git.commit.message.short} +git.commit.time=${git.commit.time} +git.closest.tag.name=${git.closest.tag.name} +git.closest.tag.commit.count=${git.closest.tag.commit.count} + +git.build.user.name=${git.build.user.name} +git.build.user.email=${git.build.user.email} +git.build.time=${git.build.time} +git.build.host=${git.build.host} +git.build.version=${git.build.version} \ No newline at end of file diff --git a/examples/clustering-soe/src/test/java/com/esri/serverextension/cluster/ClusterAssemblerIT.java b/examples/clustering-soe/src/test/java/com/esri/serverextension/cluster/ClusterAssemblerIT.java new file mode 100644 index 0000000..31afd1c --- /dev/null +++ b/examples/clustering-soe/src/test/java/com/esri/serverextension/cluster/ClusterAssemblerIT.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.cluster; + +import com.esri.arcgis.geodatabase.*; +import com.esri.arcgis.geometry.IEnvelope; +import com.esri.serverextension.core.geodatabase.FileGDBWorkspaceFactoryBean; +import com.esri.serverextension.core.geodatabase.GeodatabaseTemplate; +import com.esri.serverextension.core.rest.json.JSONGeometryMapper; +import com.esri.serverextension.core.util.ArcObjectsInitializer; +import com.esri.serverextension.core.util.ArcObjectsUtilities; +import com.esri.serverextension.core.util.StopWatch; +import com.esri.serverextension.test.AbstractArcObjectsIT; +import net.jcip.annotations.NotThreadSafe; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.List; + +@RunWith(SpringJUnit4ClassRunner.class) +@NotThreadSafe +@ContextConfiguration(locations = {"/spring/config/applicationContext-file-gdb-workspace-test.xml"}) +public class ClusterAssemblerIT extends AbstractArcObjectsIT { + + @Inject + private IWorkspace workspace; + + @Test + public void testClusterAssembler(String where) throws IOException { + System.out.println("1. Setting up query filter."); + IFeatureClass featureClass = ((IFeatureWorkspace)workspace).openFeatureClass("Permit_Features"); + SpatialFilter spatialFilter = new SpatialFilter(); + spatialFilter.setSubFields("Valuation,Shape"); + spatialFilter.setGeometryField("Shape"); + spatialFilter.setSpatialRel(esriSpatialRelEnum.esriSpatialRelIntersects); + JSONGeometryMapper geometryMapper = new JSONGeometryMapper(); + IEnvelope envelope = geometryMapper.readEnvelope("{\"xmin\":-13244092.36900171," + + "\"ymin\":4000883.3498998554," + + "\"xmax\":-13118812.079642477," + + "\"ymax\":4061574.350358204," + + "\"spatialReference\":{\"wkid\":102100}}"); + spatialFilter.setGeometryByRef(envelope); + spatialFilter.setWhereClause(where); + spatialFilter.setOutputSpatialReferenceByRef("Shape", ArcObjectsUtilities.createSpatialReference(102100)); + + System.out.println("2. Executing query."); + GeodatabaseTemplate geodatabaseTemplate = new GeodatabaseTemplate(); + ClusterAssemblerCallbackHandler clusterAssemblerCallbackHandler = new ClusterAssemblerCallbackHandler("Valuation"); + geodatabaseTemplate.query(featureClass, spatialFilter, clusterAssemblerCallbackHandler); + System.out.println(String.format("# of input features: %1$d", clusterAssemblerCallbackHandler.getFeatureCount())); + + System.out.println("3. Building clusters."); + ClusterExtent clusterExtent = new ClusterExtent(-13244092.36900171, + 4000883.3498998554, + -13118812.079642477, + 4061574.350358204); + ClusterAssembler clusterAssembler = new ClusterAssembler( + clusterAssemblerCallbackHandler.getClusterFeatures(), + 76.43702828507277, + 100, + clusterExtent); + List clusters = clusterAssembler.getClusters(); + int clusterCount = 0; + for (Cluster cluster : clusters) { + System.out.println(String.format("Cluster %1$d: (x: %2$f y: %3$f), %4$f", ++clusterCount, + cluster.getPoint().x, cluster.getPoint().y, cluster.getValue())); + } + } + + public static void main(String[] args) throws Exception { + StopWatch timer = StopWatch.createAndStart(); + ArcObjectsInitializer.getInstance().init(); + FileGDBWorkspaceFactoryBean fileGDBWorkspaceFactoryBean = new FileGDBWorkspaceFactoryBean(); + fileGDBWorkspaceFactoryBean.setDatabase("D:\\Development\\Projects\\sever-extension-java\\examples\\clustering-soe\\data\\Clustering\\Clustering.gdb"); + IWorkspace workspace = fileGDBWorkspaceFactoryBean.getObject(); + ClusterAssemblerIT clusterAssemblerIT = new ClusterAssemblerIT(); + clusterAssemblerIT.workspace = workspace; + clusterAssemblerIT.testClusterAssembler("Issue_Date >= date '2017-01-01 00:00:00'"); + ArcObjectsInitializer.getInstance().shutdown(); + System.out.println(String.format("Time elapsed: %1$f", timer.stop().elapsedTimeSeconds())); + } +} diff --git a/examples/clustering-soe/src/test/java/com/esri/serverextension/test/AbstractArcObjectsIT.java b/examples/clustering-soe/src/test/java/com/esri/serverextension/test/AbstractArcObjectsIT.java new file mode 100644 index 0000000..efcff24 --- /dev/null +++ b/examples/clustering-soe/src/test/java/com/esri/serverextension/test/AbstractArcObjectsIT.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.test; + +import com.esri.arcgis.interop.AutomationException; +import com.esri.serverextension.core.util.ArcObjectsInitializer; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.net.UnknownHostException; + +public abstract class AbstractArcObjectsIT { + + public AbstractArcObjectsIT() { + } + + @BeforeClass + public static void init() throws UnknownHostException, IOException { + ArcObjectsInitializer.getInstance().init(); + } + + @AfterClass + public static void shutdown() throws AutomationException, IOException { + ArcObjectsInitializer.getInstance().shutdown(); + } +} diff --git a/examples/clustering-soe/src/test/resources/log4j.xml b/examples/clustering-soe/src/test/resources/log4j.xml new file mode 100644 index 0000000..f5bacf5 --- /dev/null +++ b/examples/clustering-soe/src/test/resources/log4j.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/examples/clustering-soe/src/test/resources/spring/config/applicationContext-file-gdb-workspace-test.xml b/examples/clustering-soe/src/test/resources/spring/config/applicationContext-file-gdb-workspace-test.xml new file mode 100644 index 0000000..6f29500 --- /dev/null +++ b/examples/clustering-soe/src/test/resources/spring/config/applicationContext-file-gdb-workspace-test.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 997b1cb..8283c61 100644 --- a/pom.xml +++ b/pom.xml @@ -23,8 +23,9 @@ pom server-extension-core - examples/attribute-security-filter-soi server-extension-template + examples/attribute-security-filter-soi + examples/clustering-soe 1.8 @@ -32,7 +33,6 @@ 4.3.6.RELEASE UTF-8 UTF-8 - **/*IT.java ${env.JAVA_HOME} @@ -57,11 +57,46 @@ 4.12 test + + org.slf4j + slf4j-api + 1.7.24 + + + ch.qos.logback + logback-core + 1.2.1 + + + org.apache.commons + commons-lang3 + 3.5 + + org.springframework spring-test test + + log4j + log4j + 1.2.17 + test + + + org.slf4j + jcl-over-slf4j + 1.7.24 + test + + + + net.jcip + jcip-annotations + 1.0 + test + @@ -99,8 +134,10 @@ ArcObject's classes. It requires a properly activated installation of ArcGIS for Sever. --> -Djava.library.path="${env.AGSSERVER}/bin" + 1 + true - ${failsafe.exclude}/ + **/*IT.java diff --git a/server-extension-core/pom.xml b/server-extension-core/pom.xml index c46d920..520302b 100644 --- a/server-extension-core/pom.xml +++ b/server-extension-core/pom.xml @@ -25,21 +25,6 @@ server-extension-core Shared library for building SOEs and SOIs - - org.slf4j - slf4j-api - 1.7.24 - - - ch.qos.logback - logback-core - 1.2.1 - - - org.apache.commons - commons-lang3 - 3.5 - commons-io commons-io @@ -67,6 +52,10 @@ + + org.springframework + spring-jdbc + javax.enterprise cdi-api @@ -75,17 +64,17 @@ com.fasterxml.jackson.core jackson-annotations - 2.8.7 + 2.6.7 com.fasterxml.jackson.jaxrs jackson-jaxrs-base - 2.8.7 + 2.6.7 com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider - 2.8.7 + 2.6.7 org.glassfish.jersey.core @@ -99,17 +88,5 @@ system ${env.AGSSERVER}\framework\lib\arcobjects.jar - - - org.springframework - spring-test - test - - - org.slf4j - jcl-over-slf4j - 1.7.24 - test - \ No newline at end of file diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/FileGDBWorkspaceFactoryBean.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/FileGDBWorkspaceFactoryBean.java new file mode 100644 index 0000000..6cbd59d --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/FileGDBWorkspaceFactoryBean.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.SmartFactoryBean; + +import com.esri.arcgis.datasourcesGDB.FileGDBWorkspaceFactory; +import com.esri.arcgis.geodatabase.IWorkspace; +import com.esri.arcgis.system.Cleaner; +import com.esri.arcgis.system.IPropertySet; +import com.esri.arcgis.system.PropertySet; + +public class FileGDBWorkspaceFactoryBean implements + SmartFactoryBean, DisposableBean { + + private String database; + private IWorkspace workspace; + + public FileGDBWorkspaceFactoryBean() { + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + @Override + public IWorkspace getObject() throws Exception { + if (workspace == null) { + IPropertySet propertySet = new PropertySet(); + if (database != null) { + propertySet.setProperty("DATABASE", database); + } + FileGDBWorkspaceFactory fileGDBWorkspaceFactory = new FileGDBWorkspaceFactory(); + workspace = fileGDBWorkspaceFactory.open(propertySet, 0); + + // FileGDBWorkspaceFactory is a singleton. + // It is a good practice to release the singletons + // using com.esri.system.Cleaner.release() + Cleaner.release(fileGDBWorkspaceFactory); + } + return workspace; + } + + @Override + public Class getObjectType() { + return IWorkspace.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public boolean isEagerInit() { + return false; + } + + @Override + public boolean isPrototype() { + return false; + } + + @Override + public void destroy() throws Exception { + if (workspace != null) { + Cleaner.release(workspace); + workspace = null; + } + } + + @Override + public String toString() { + return "FileGDBWorkspaceFactoryBean [database=" + database + "]"; + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseCursorExtractor.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseCursorExtractor.java new file mode 100644 index 0000000..ca29564 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseCursorExtractor.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import java.io.IOException; + +import com.esri.arcgis.geodatabase.ICursor; +import com.esri.arcgis.geodatabase.IFeatureCursor; +import com.esri.arcgis.geodatabase.IField; + +public interface GeodatabaseCursorExtractor { + + T extractData(ICursor cursor, GeodatabaseFieldMap fieldMap) throws IOException; + + T extractData(IFeatureCursor featureCursor, GeodatabaseFieldMap fieldMap) throws IOException; +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseFieldMap.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseFieldMap.java new file mode 100644 index 0000000..dd6e6a2 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseFieldMap.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import com.esri.arcgis.geodatabase.IFeatureClass; +import com.esri.arcgis.geodatabase.IField; +import com.esri.arcgis.geodatabase.IFields; +import com.esri.arcgis.geodatabase.IQueryFilter; +import com.esri.serverextension.core.util.ArcObjectsInteropException; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.*; + +public class GeodatabaseFieldMap { + + private Map fieldsIndicesByName; + + public GeodatabaseFieldMap() { + + } + + public Collection getFieldIndices() { + return Collections.unmodifiableCollection(fieldsIndicesByName.values()); + } + + public Set getFieldNames() { + return Collections.unmodifiableSet(fieldsIndicesByName.keySet()); + } + + public FieldIndex get(String fieldName) { + return fieldsIndicesByName.get(fieldName); + } + + public void initialize(IFields fields, String subFields) { + try { + String[] subFieldsArr = null; + if (StringUtils.isNotEmpty(subFields) || !"*".equals(subFields)) { + subFieldsArr = subFields.split(","); + } + int fieldCount = fields.getFieldCount(); + fieldsIndicesByName = new LinkedHashMap<>(); + for (int i = 0; i < fieldCount; i++) { + if (subFieldsArr != null) { + for (String subField : subFieldsArr) { + if (subField.equals(fields.getField(i).getName())) { + fieldsIndicesByName.put( + fields.getField(i).getName(), + new FieldIndex( + fields.getField(i), + i + ) + ); + break; + } + } + } else { + fieldsIndicesByName.put( + fields.getField(i).getName(), + new FieldIndex( + fields.getField(i), + i + ) + ); + } + } + } catch (IOException ex) { + throw new ArcObjectsInteropException("Failed to generate field index map."); + } + } + + public static class FieldIndex { + private IField field; + private int index; + + public FieldIndex(IField field, int index) { + this.field = field; + this.index = index; + } + + public IField getField() { + return field; + } + + public int getIndex() { + return index; + } + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectCallbackHandler.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectCallbackHandler.java new file mode 100644 index 0000000..400d4a3 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectCallbackHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import com.esri.arcgis.geodatabase.IFeature; +import com.esri.arcgis.geodatabase.IField; +import com.esri.arcgis.geodatabase.IRow; + +import java.io.IOException; + +public interface GeodatabaseObjectCallbackHandler { + + public void setGeodatabaseFieldMap(GeodatabaseFieldMap fieldMap) throws IOException; + + public void processRow(IRow row) throws IOException; + + public void processFeature(IFeature feature) throws IOException; +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectCallbackHandlerCursorExtractor.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectCallbackHandlerCursorExtractor.java new file mode 100644 index 0000000..5fd57b0 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectCallbackHandlerCursorExtractor.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import com.esri.arcgis.geodatabase.*; +import com.esri.arcgis.interop.AutomationException; +import com.esri.arcgis.system.Cleaner; +import com.esri.serverextension.core.util.StopWatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +import java.io.IOException; + +public class GeodatabaseObjectCallbackHandlerCursorExtractor implements + GeodatabaseCursorExtractor { + + private final Logger logger = LoggerFactory + .getLogger(GeodatabaseObjectMapperCursorExtractor.class); + + private final GeodatabaseObjectCallbackHandler objectCallbackHandler; + private final int objectsExpected; + + public GeodatabaseObjectCallbackHandlerCursorExtractor(GeodatabaseObjectCallbackHandler objectCallbackHandler) { + this(objectCallbackHandler, -1); + } + + public GeodatabaseObjectCallbackHandlerCursorExtractor(GeodatabaseObjectCallbackHandler objectCallbackHandler, int objectsExpected) { + this.objectCallbackHandler = objectCallbackHandler; + this.objectsExpected = objectsExpected; + } + + @Override + public Object extractData(ICursor cursor, GeodatabaseFieldMap fieldMap) + throws IOException { + try { + StopWatch stopWatch = StopWatch.createAndStart(); + + objectCallbackHandler.setGeodatabaseFieldMap(fieldMap); + + IRow row = null; + int rowCount = 0; + if ((row = cursor.nextRow()) != null) { + logger.debug("Executing query took {} second(s).", stopWatch + .stop().elapsedTimeSeconds()); + stopWatch.start(); + rowCount++; + objectCallbackHandler.processRow(row); + Cleaner.release(row); + } else { + logger.debug("Query returned no rows."); + return null; + } + if (objectsExpected == 1) { + if ((row = cursor.nextRow()) != null) { + Cleaner.release(row); + throw new IncorrectResultSizeDataAccessException( + "Expected just one row but found more.", 1); + } + } else { + while ((row = cursor.nextRow()) != null) { + rowCount++; + objectCallbackHandler.processRow(row); + Cleaner.release(row); + } + } + logger.debug("Extracting {} row(s) took {} second(s).", rowCount, + stopWatch.stop().elapsedTimeSeconds()); + return null; + } catch (AutomationException ex) { + throw new GeodatabaseSystemException("Failed to extract data from cursor.", + ex); + } finally { + if (cursor != null) { + Cleaner.release(cursor); + } + } + } + + @Override + public Object extractData(IFeatureCursor featureCursor, GeodatabaseFieldMap fieldMap) throws IOException { + try { + StopWatch stopWatch = StopWatch.createAndStart(); + + objectCallbackHandler.setGeodatabaseFieldMap(fieldMap); + + IFeature feature = null; + int featureCount = 0; + if ((feature = featureCursor.nextFeature()) != null) { + logger.debug("Executing query took {} second(s).", stopWatch + .stop().elapsedTimeSeconds()); + stopWatch.start(); + featureCount++; + objectCallbackHandler.processFeature(feature); + Cleaner.release(feature); + } else { + logger.debug("Query returned no features."); + return null; + } + if (objectsExpected == 1) { + if ((feature = featureCursor.nextFeature()) != null) { + Cleaner.release(feature); + throw new IncorrectResultSizeDataAccessException( + "Expected just one feature but found more.", 1); + } + } else { + while ((feature = featureCursor.nextFeature()) != null) { + featureCount++; + objectCallbackHandler.processFeature(feature); + Cleaner.release(feature); + } + } + logger.debug("Extracting {} features(s) took {} second(s).", featureCount, + stopWatch.stop().elapsedTimeSeconds()); + return null; + } catch (AutomationException ex) { + throw new GeodatabaseSystemException("Failed to extract data from cursor.", + ex); + } finally { + if (featureCursor != null) { + Cleaner.release(featureCursor); + } + } + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectMapper.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectMapper.java new file mode 100644 index 0000000..4bd84d9 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectMapper.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import java.io.IOException; + +import com.esri.arcgis.geodatabase.IFeature; +import com.esri.arcgis.geodatabase.IFeatureClass; +import com.esri.arcgis.geodatabase.IField; +import com.esri.arcgis.geodatabase.IRow; + +public interface GeodatabaseObjectMapper { + + public void setGeodatabaseFieldMap(GeodatabaseFieldMap fieldMap) throws IOException; + + public T mapRow(IRow row) throws IOException; + + public T mapFeature(IFeature feature) throws IOException; + +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectMapperCursorExtractor.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectMapperCursorExtractor.java new file mode 100644 index 0000000..4e91d01 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseObjectMapperCursorExtractor.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import com.esri.arcgis.geodatabase.*; +import com.esri.arcgis.interop.AutomationException; +import com.esri.arcgis.system.Cleaner; +import com.esri.serverextension.core.util.StopWatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GeodatabaseObjectMapperCursorExtractor implements + GeodatabaseCursorExtractor> { + + private final Logger logger = LoggerFactory + .getLogger(GeodatabaseObjectMapperCursorExtractor.class); + + private final GeodatabaseObjectMapper objectMapper; + private final int objectsExpected; + + public GeodatabaseObjectMapperCursorExtractor(GeodatabaseObjectMapper objectMapper) { + this(objectMapper, -1); + } + + public GeodatabaseObjectMapperCursorExtractor(GeodatabaseObjectMapper objectMapper, int objectsExpected) { + this.objectMapper = objectMapper; + this.objectsExpected = objectsExpected; + } + + @Override + public List extractData(ICursor cursor, GeodatabaseFieldMap fieldMap) + throws IOException { + try { + StopWatch stopWatch = StopWatch.createAndStart(); + + objectMapper.setGeodatabaseFieldMap(fieldMap); + + List resultSet = objectsExpected > 0 ? new ArrayList( + objectsExpected) : new ArrayList(); + IRow row = null; + int rowCount = 0; + if ((row = cursor.nextRow()) != null) { + logger.debug("Executing query took {} second(s).", stopWatch + .stop().elapsedTimeSeconds()); + stopWatch.start(); + rowCount++; + resultSet.add(objectMapper.mapRow(row)); + Cleaner.release(row); + } else { + logger.debug("Query returned no rows."); + return resultSet; + } + if (objectsExpected == 1) { + if ((row = cursor.nextRow()) != null) { + Cleaner.release(row); + throw new IncorrectResultSizeDataAccessException( + "Expected just one row but found more.", 1); + } + } else { + while ((row = cursor.nextRow()) != null) { + rowCount++; + resultSet.add(objectMapper.mapRow(row)); + Cleaner.release(row); + } + } + logger.debug("Extracting {} row(s) took {} second(s).", rowCount, + stopWatch.stop().elapsedTimeSeconds()); + return resultSet; + } catch (AutomationException ex) { + throw new GeodatabaseSystemException("Failed to extract data from cursor.", + ex); + } finally { + if (cursor != null) { + Cleaner.release(cursor); + } + } + } + + @Override + public List extractData(IFeatureCursor featureCursor, GeodatabaseFieldMap fieldMap) throws IOException { + try { + StopWatch stopWatch = StopWatch.createAndStart(); + + objectMapper.setGeodatabaseFieldMap(fieldMap); + + List resultSet = objectsExpected > 0 ? new ArrayList( + objectsExpected) : new ArrayList(); + IFeature feature = null; + int featureCount = 0; + if ((feature = featureCursor.nextFeature()) != null) { + logger.debug("Executing query took {} second(s).", stopWatch + .stop().elapsedTimeSeconds()); + stopWatch.start(); + featureCount++; + resultSet.add(objectMapper.mapRow(feature)); + Cleaner.release(feature); + } else { + logger.debug("Query returned no features."); + return resultSet; + } + if (objectsExpected == 1) { + if ((feature = featureCursor.nextFeature()) != null) { + Cleaner.release(feature); + throw new IncorrectResultSizeDataAccessException( + "Expected just one feature but found more.", 1); + } + } else { + while ((feature = featureCursor.nextFeature()) != null) { + featureCount++; + resultSet.add(objectMapper.mapRow(feature)); + Cleaner.release(feature); + } + } + logger.debug("Extracting {} features(s) took {} second(s).", featureCount, + stopWatch.stop().elapsedTimeSeconds()); + return resultSet; + } catch (AutomationException ex) { + throw new GeodatabaseSystemException("Failed to extract data from cursor.", + ex); + } finally { + if (featureCursor != null) { + Cleaner.release(featureCursor); + } + } + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseSystemException.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseSystemException.java new file mode 100644 index 0000000..240d34b --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseSystemException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import org.springframework.dao.UncategorizedDataAccessException; + +import com.esri.arcgis.interop.AutomationException; + +public class GeodatabaseSystemException extends UncategorizedDataAccessException { + + private static final long serialVersionUID = 1L; + + public GeodatabaseSystemException(AutomationException cause) { + this(null, cause); + } + + public GeodatabaseSystemException(String msg, AutomationException cause) { + super(msg, cause); + } + +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseTemplate.java b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseTemplate.java new file mode 100644 index 0000000..716733b --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/geodatabase/GeodatabaseTemplate.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.geodatabase; + +import com.esri.arcgis.geodatabase.*; +import com.esri.arcgis.interop.AutomationException; +import com.esri.arcgis.system.Cleaner; +import com.esri.serverextension.core.util.StopWatch; +import com.esri.serverextension.core.util.UncheckedIOException; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GeodatabaseTemplate { + + private final Logger logger = LoggerFactory.getLogger(GeodatabaseTemplate.class); + + public GeodatabaseTemplate() { + } + + public List query(IFeatureClass featureClass, IQueryFilter queryFilter, GeodatabaseObjectMapper objectMapper) { + return query(featureClass, queryFilter, new GeodatabaseObjectMapperCursorExtractor(objectMapper)); + } + + public T queryForObject(IFeatureClass featureClass, IQueryFilter queryFilter, + GeodatabaseObjectMapper objectMapper) { + List resultSet = query(featureClass, queryFilter, new GeodatabaseObjectMapperCursorExtractor( + objectMapper, 1)); + return resultSet.size() == 1 ? resultSet.get(0) : null; + } + + public void query(IFeatureClass featureClass, IQueryFilter queryFilter, + GeodatabaseObjectCallbackHandler objectCallbackHandler) { + query(featureClass, queryFilter, new GeodatabaseObjectCallbackHandlerCursorExtractor( + objectCallbackHandler)); + } + + public T query(IFeatureClass featureClass, IQueryFilter queryFilter, + GeodatabaseCursorExtractor cursorExtractor) { + IFeatureCursor cursor = null; + T result = null; + try { + StopWatch stopWatch = StopWatch.createAndStart(); + cursor = featureClass.search(queryFilter, false); + logger.debug("Preparing query took {} second(s).", stopWatch.stop() + .elapsedTimeSeconds()); + stopWatch.start(); + GeodatabaseFieldMap fieldMap = new GeodatabaseFieldMap(); + fieldMap.initialize(cursor.getFields(), queryFilter != null ? queryFilter.getSubFields() : null); + result = cursorExtractor.extractData(cursor, fieldMap); + logger.debug( + "Executing query and extracting data from cursor took {} second(s).", + stopWatch.stop().elapsedTimeSeconds()); + } catch (IOException ex) { + throw new UncheckedIOException("Failed to execute query.", ex); + } finally { + if (cursor != null) { + Cleaner.release(cursor); + } + } + return result; + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/DimensionalDefinition.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/DimensionalDefinition.java index 20bccb3..c9c1e63 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/DimensionalDefinition.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/DimensionalDefinition.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; @@ -68,4 +69,9 @@ public boolean isSlice() { public void setSlice(boolean slice) { this.slice = slice; } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } } diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Extent.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Extent.java new file mode 100644 index 0000000..ce32cfb --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Extent.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.rest.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class Extent implements Serializable { + + private static final long serialVersionUID = 1L; + + private Double xmin; + private Double ymin; + private Double xmax; + private Double ymax; + + public Extent() { + + } + + public Double getXmin() { + return xmin; + } + + public void setXmin(Double xmin) { + this.xmin = xmin; + } + + public Double getYmin() { + return ymin; + } + + public void setYmin(Double ymin) { + this.ymin = ymin; + } + + public Double getXmax() { + return xmax; + } + + public void setXmax(Double xmax) { + this.xmax = xmax; + } + + public Double getYmax() { + return ymax; + } + + public void setYmax(Double ymax) { + this.ymax = ymax; + } + + public double getWidth() { + return xmax - xmin; + } + + public double getHeight() { + return ymax - ymin; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} \ No newline at end of file diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Feature.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Feature.java new file mode 100644 index 0000000..c4cb492 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Feature.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.rest.api; + + +import com.esri.arcgis.geometry.IGeometry; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Map; + +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class Feature implements Serializable { + + private static final long serialVersionUID = 1L; + + private IGeometry geometry; + private Map attributes; + + public Feature() { + } + + public IGeometry getGeometry() { + return geometry; + } + + public void setGeometry(IGeometry geometry) { + this.geometry = geometry; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/FeatureSet.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/FeatureSet.java new file mode 100644 index 0000000..ae6f4e0 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/FeatureSet.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.rest.api; + +import com.esri.arcgis.geometry.ISpatialReference; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; + +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class FeatureSet implements Serializable { + + private static final long serialVersionUID = 1L; + + private String objectIdFieldName; //optional + private String globalIdFieldName; //optional + private String displayFieldName; //optional + private GeometryType geometryType; //for feature layers only + private ISpatialReference spatialReference; //for feature layers only + @JsonProperty("hasZ") + private boolean hasZ; //optional Default is false. + @JsonProperty("hasM") + private boolean hasM; //optional Default is false. + private Map fieldAliases; + private List fields; + private List features; //features will include geometry for feature layers only + + public FeatureSet() { + + } + + public String getObjectIdFieldName() { + return objectIdFieldName; + } + + public void setObjectIdFieldName(String objectIdFieldName) { + this.objectIdFieldName = objectIdFieldName; + } + + public String getGlobalIdFieldName() { + return globalIdFieldName; + } + + public void setGlobalIdFieldName(String globalIdFieldName) { + this.globalIdFieldName = globalIdFieldName; + } + + public String getDisplayFieldName() { + return displayFieldName; + } + + public void setDisplayFieldName(String displayFieldName) { + this.displayFieldName = displayFieldName; + } + + public GeometryType getGeometryType() { + return geometryType; + } + + public void setGeometryType(GeometryType geometryType) { + this.geometryType = geometryType; + } + + public ISpatialReference getSpatialReference() { + return spatialReference; + } + + public void setSpatialReference(ISpatialReference spatialReference) { + this.spatialReference = spatialReference; + } + + public boolean isHasZ() { + return hasZ; + } + + public void setHasZ(boolean hasZ) { + this.hasZ = hasZ; + } + + public boolean isHasM() { + return hasM; + } + + public void setHasM(boolean hasM) { + this.hasM = hasM; + } + + public Map getFieldAliases() { + return fieldAliases; + } + + public void setFieldAliases(Map fieldAliases) { + this.fieldAliases = fieldAliases; + } + + public List getFields() { + return fields; + } + + public void setFields(List fields) { + this.fields = fields; + } + + public List getFeatures() { + return features; + } + + public void setFeatures(List features) { + this.features = features; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Field.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Field.java new file mode 100644 index 0000000..5f60b0a --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Field.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.rest.api; + +import java.io.Serializable; + +import javax.xml.bind.annotation.XmlRootElement; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.commons.lang3.builder.ToStringBuilder; + +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class Field implements Serializable { + + private static final long serialVersionUID = 1L; + + private String name; + private FieldType type; + private String alias; + private Integer length; + + public Field() { + } + + public Field(String name, String alias) { + this.name = name; + this.alias = alias; + } + + public Field(String name, FieldType type, String alias) { + this.name = name; + this.type = type; + this.alias = alias; + } + + public Field(String name, FieldType type, String alias, Integer length) { + this.name = name; + this.type = type; + this.alias = alias; + this.length = length; + } + + public String getName() { + return name; + } + + public FieldType getType() { + return type; + } + + public String getAlias() { + return alias; + } + + public Integer getLength() { + return length; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/FieldType.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/FieldType.java new file mode 100644 index 0000000..6ae9ba4 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/FieldType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.rest.api; + +import java.io.Serializable; + +import javax.xml.bind.annotation.XmlType; + +@XmlType +public enum FieldType implements Serializable { + esriFieldTypeSmallInteger, esriFieldTypeInteger, esriFieldTypeSingle, esriFieldTypeDouble, esriFieldTypeString, esriFieldTypeDate, esriFieldTypeOID, esriFieldTypeGeometry, esriFieldTypeBlob, esriFieldTypeRaster, esriFieldTypeGUID, esriFieldTypeGlobalID, esriFieldTypeXML +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/QueryMapServiceLayerOperationInput.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/QueryMapServiceLayerOperationInput.java index b90d3dd..0e5dae1 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/QueryMapServiceLayerOperationInput.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/QueryMapServiceLayerOperationInput.java @@ -14,13 +14,21 @@ package com.esri.serverextension.core.rest.api; +import com.esri.arcgis.geodatabase.IQueryFilter; +import com.esri.arcgis.geodatabase.ISpatialFilter; +import com.esri.arcgis.geodatabase.SpatialFilter; +import com.esri.arcgis.geometry.esriSpatialRelationEnum; import com.esri.arcgis.geometry.IGeometry; import com.esri.arcgis.geometry.ISpatialReference; +import com.esri.serverextension.core.util.ArcObjectsInteropException; +import com.esri.serverextension.core.util.GenericEsriEnum; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import javax.xml.bind.annotation.XmlRootElement; +import java.io.IOException; import java.util.List; import java.util.Map; @@ -333,4 +341,5 @@ public void setRelationParam(String relationParam) { public String toString() { return ToStringBuilder.reflectionToString(this); } + } diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RangeValue.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RangeValue.java index 34bc2ee..bf1eaf6 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RangeValue.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RangeValue.java @@ -15,6 +15,7 @@ package com.esri.serverextension.core.rest.api; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.commons.lang3.builder.ToStringBuilder; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; @@ -46,4 +47,9 @@ public Object getValue() { public void setValue(Object value) { this.value = value; } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } } diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RenderingRule.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RenderingRule.java index 3bade7d..575e3c9 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RenderingRule.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/RenderingRule.java @@ -15,6 +15,7 @@ package com.esri.serverextension.core.rest.api; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.commons.lang3.builder.ToStringBuilder; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; @@ -68,4 +69,9 @@ public String getVariableName() { public void setVariableName(String variableName) { this.variableName = variableName; } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } } diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Transformation.java b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Transformation.java index 6b8d020..e7b35a0 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Transformation.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/rest/api/Transformation.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; @@ -48,4 +49,9 @@ public boolean isTransformForward() { public void setTransformForward(boolean transformForward) { this.transformForward = transformForward; } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } } diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/server/AbstractRestServerObjectExtension.java b/server-extension-core/src/main/java/com/esri/serverextension/core/server/AbstractRestServerObjectExtension.java index 14ee2c8..6388ca4 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/server/AbstractRestServerObjectExtension.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/server/AbstractRestServerObjectExtension.java @@ -82,6 +82,9 @@ protected String getDisplayName() { public final void init(IServerObjectHelper serverObjectHelper) throws IOException, AutomationException { try { + // prevents server extension from failing to shut down + Cleaner.trackObjectsInCurrentThread(); + ServerObjectExtProperties annotation = this.getClass() .getAnnotation(ServerObjectExtProperties.class); isInterceptor = annotation.interceptor(); @@ -105,6 +108,10 @@ public final void shutdown() throws IOException, AutomationException { logger.info("Shutting down ..."); doShutdown(); ((ConfigurableApplicationContext) applicationContext).close(); + + // prevents server extension from failing to shut down + Cleaner.releaseAllInCurrentThread(); + logger.info("Shut down completed."); delegateMappings = null; serverContext = null; diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/server/DefaultConfig.java b/server-extension-core/src/main/java/com/esri/serverextension/core/server/DefaultConfig.java index ca32330..fbf08a9 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/server/DefaultConfig.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/server/DefaultConfig.java @@ -26,7 +26,7 @@ import javax.inject.Singleton; @Configuration -@ComponentScan("com.esri.arcgis.soe.template") +@ComponentScan("com.esri.serverextension.core") public class DefaultConfig { @Inject diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DefaultReturnValueHandler.java b/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DefaultReturnValueHandler.java index 13df8ce..90fdb62 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DefaultReturnValueHandler.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DefaultReturnValueHandler.java @@ -15,6 +15,8 @@ package com.esri.serverextension.core.server.internal; import com.esri.arcgis.geodatabase.IRecordSet; +import com.esri.arcgis.server.json.JSONArray; +import com.esri.arcgis.server.json.JSONObject; import com.esri.serverextension.core.server.RestRequest; import com.esri.serverextension.core.server.RestResponse; import com.esri.serverextension.core.rest.json.JSONConverter; @@ -24,6 +26,7 @@ import org.springframework.core.MethodParameter; import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; public final class DefaultReturnValueHandler implements ReturnValueHandler { @@ -41,7 +44,7 @@ public RestResponse handleReturnValue(Object returnValue, .getAnnotation(XmlRootElement.class); if (RestResponse.class.isAssignableFrom(type)) { return (RestResponse) returnValue; - } else if (String.class.isAssignableFrom(type)) { + } else if (CharSequence.class.isAssignableFrom(type)) { return new RestResponse(null, JSONConverter.toByteArray((String) returnValue)); } else if (xmlRootElement != null) { @@ -50,14 +53,17 @@ public RestResponse handleReturnValue(Object returnValue, } else if (IRecordSet.class.isAssignableFrom(type)) { return new RestResponse(null, objectMapper.writeValueAsBytes(returnValue)); - } else if (IJSONObject.class.isAssignableFrom(type)) { + } else if (JSONObject.class.isAssignableFrom(type)) { return new RestResponse(null, - JSONConverter.toByteArray(((IJSONObject) returnValue) - .toJSONString(null))); - } else if (IJSONArray.class.isAssignableFrom(type)) { + JSONConverter.toByteArray(((JSONObject) returnValue) + .toString())); + } else if (JSONArray.class.isAssignableFrom(type)) { return new RestResponse(null, - JSONConverter.toByteArray(((IJSONArray) returnValue) - .toJSONString(null))); + JSONConverter.toByteArray(((JSONArray) returnValue) + .toString())); + } else if (Collection.class.isAssignableFrom(type)) { + return new RestResponse(null, + objectMapper.writeValueAsBytes(returnValue)); } throw new IllegalArgumentException(String.format( "Cannot handle return value: %1$s", returnValue)); @@ -70,12 +76,15 @@ public boolean handles(MethodParameter parameter) { "Method parameter must be a return value."); } Class type = parameter.getParameterType(); - return RestResponse.class.isAssignableFrom(type) + return RestRequest.class.isAssignableFrom(type) + || RestResponse.class.isAssignableFrom(type) + || CharSequence.class.isAssignableFrom(type) || IRecordSet.class.isAssignableFrom(type) || type.getAnnotation(XmlRootElement.class) != null || IRecordSet.class.isAssignableFrom(type) - || IJSONObject.class.isAssignableFrom(type) - || IJSONArray.class.isAssignableFrom(type); + || JSONObject.class.isAssignableFrom(type) + || JSONArray.class.isAssignableFrom(type) + || Collection.class.isAssignableFrom(type); } } diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DelegateMethodInvoker.java b/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DelegateMethodInvoker.java index c280ff7..0c0b702 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DelegateMethodInvoker.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/DelegateMethodInvoker.java @@ -88,7 +88,7 @@ public RestResponse process(RestRequest request, RestDelegate handler) { + "delegate method '%2$s' in '%3$s' with the following " + "arguments: %4$s", returnValue, methodName, className, Arrays.toString(arguments)); - throw new ServerObjectExtensionException(message, ex.getCause()); + throw new ServerObjectExtensionException(message, ex); } return response; diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/PathVariableArgumentResolver.java b/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/PathVariableArgumentResolver.java index 2dd5fa5..bee0a6d 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/PathVariableArgumentResolver.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/server/internal/PathVariableArgumentResolver.java @@ -16,8 +16,10 @@ import com.esri.serverextension.core.server.RestDelegate; import com.esri.serverextension.core.server.RestRequest; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.SimpleTypeConverter; import org.springframework.core.MethodParameter; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.util.UriTemplate; @@ -50,12 +52,24 @@ public Object resolveArgument(MethodParameter parameter, if (uriTemplate.matches(path)) { Map uriTemplateVars = uriTemplate.match(path); if (uriTemplateVars != null) { - value = uriTemplateVars.get(variable); + if (StringUtils.isEmpty(variable)) { + // PathVariable does not contain name + // get first value in list + if (!CollectionUtils.isEmpty(uriTemplateVars)) { + value = uriTemplateVars.values().iterator().next(); + } + } else { + value = uriTemplateVars.get(variable); + } break; } } } + if (value == null) { + return value; + } + SimpleTypeConverter converter = new SimpleTypeConverter(); return converter.convertIfNecessary(value, type, parameter); } diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/util/GenericEsriEnum.java b/server-extension-core/src/main/java/com/esri/serverextension/core/util/GenericEsriEnum.java new file mode 100644 index 0000000..ba649e3 --- /dev/null +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/util/GenericEsriEnum.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2017 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.esri.serverextension.core.util; + +import com.esri.serverextension.core.rest.json.JSONException; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class GenericEsriEnum { + + private GenericEsriEnum() { + } + + /** + * Deserializer for Esri-style enums, which are interfaces with public static + * int members, e.g. com.esri.arcgis.geometry.esriGeometryType. This method converts + * the string representation of one of the fields, e.g. esriGeometryPoint, to + * its equivalent int representation, e.g. 1, given the specified Esri-style enum class. + * + * @param esriEnum The Esri-style enum class used to deserialize a string value. + * @param enumValue The name of a public static integer field in the Esri-style enum class. + * @param The Esri-style enum class used to deserialize a string value. + * @return the integer value for the specified string + */ + public static final Integer valueOf(Class esriEnum, String enumValue) { + Field[] fields = esriEnum.getDeclaredFields(); + for (Field field : fields) { + if (!Modifier.isStatic(field.getModifiers())) { + continue; + } + if (!Modifier.isPublic(field.getModifiers())) { + continue; + } + if (!field.getType().equals(Integer.TYPE)) { + continue; + } + if (field.getName().equals(enumValue)) { + try { + return field.getInt(null); + } catch (IllegalAccessException ex) { + throw new JSONException("Cannot access property.", ex); + } + } + } + throw new IllegalArgumentException(String.format( + "Cannot find field %1$s in class %2$s", enumValue, esriEnum.getName())); + } +} diff --git a/server-extension-core/src/main/java/com/esri/serverextension/core/util/StopWatch.java b/server-extension-core/src/main/java/com/esri/serverextension/core/util/StopWatch.java index 27bc236..d063301 100644 --- a/server-extension-core/src/main/java/com/esri/serverextension/core/util/StopWatch.java +++ b/server-extension-core/src/main/java/com/esri/serverextension/core/util/StopWatch.java @@ -30,22 +30,24 @@ public static StopWatch createAndStart() { return timer; } - public void start() { + public StopWatch start() { if (isRunning) { throw new IllegalStateException("StopWatch is already running."); } isRunning = true; hasElapsed = false; startTime = System.currentTimeMillis(); + return this; } - public void stop() { + public StopWatch stop() { if (!isRunning) { throw new IllegalStateException("StopWatch is not running."); } elapsedTime = System.currentTimeMillis() - startTime; isRunning = false; hasElapsed = true; + return this; } public long elapsedTimeMillis() { @@ -53,7 +55,7 @@ public long elapsedTimeMillis() { throw new IllegalStateException("StopWatch is already running."); } if (!hasElapsed) { - throw new IllegalStateException("StopWatch was not run."); + throw new IllegalStateException("StopWatch has never been started."); } return elapsedTime; } diff --git a/server-extension-template/pom.xml b/server-extension-template/pom.xml index 2823a90..ca0b51f 100644 --- a/server-extension-template/pom.xml +++ b/server-extension-template/pom.xml @@ -29,7 +29,7 @@ com.esri.serverextensions server-extension-core - ${parent.version} + ${project.parent.version}