Point of Interest Finder REST Server Object Extension
arcgissamples\soe\POIFinderSOE.java
/* Copyright 2010 ESRI
* 
* All rights reserved under the copyright laws of the United States
* and applicable international laws, treaties, and conventions.
* 
* You may freely redistribute and use this sample code, with or
* without modification, provided you include the original copyright
* notice and use restrictions.
* 
* See the use restrictions.
* 
*/
package arcgissamples.soe;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import com.esri.arcgis.carto.IFeatureLayer;
import com.esri.arcgis.carto.Map;
import com.esri.arcgis.carto.MapServer;
import com.esri.arcgis.geodatabase.FeatureClass;
import com.esri.arcgis.geodatabase.FeatureCursor;
import com.esri.arcgis.geodatabase.IFeature;
import com.esri.arcgis.geodatabase.QueryFilter;
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.IServerObjectExtension;
import com.esri.arcgis.server.IServerObjectHelper;
import com.esri.arcgis.system.ILog;
import com.esri.arcgis.system.IRESTRequestHandler;
import com.esri.arcgis.system.ServerUtilities;

import static com.esri.arcgis.system.ServerUtilities.*;

import com.esri.arcgis.server.json.*;

@ArcGISExtension
@ServerObjectExtProperties(
    displayName = "Points of Interest Finder SOE", 
    description = "Finds points of interest such as gas stations and restaurants",
    defaultSOAPCapabilities = { "Find Gas Stations", "Find Restaurants" }, 
    allSOAPCapabilities = { "Find Gas Stations", "Find Restaurants" },
    supportsMSD = true
)
public class POIFinderSOE implements IServerObjectExtension, IRESTRequestHandler
{
  private static final long serialVersionUID = 1L;
  private IServerObjectHelper soHelper;
  private ILog serverLog;
  private MapServer mapServer;
  private FeatureClass poiFC;

  public POIFinderSOE() throws Exception
  {
    super();
  }

  /****************************************************************************************************************************
   * IServerObjectExtension methods: This is a mandatory interface that must be supported by all SOEs. This interface
   * is used by the Server Object to manage the lifetime of the SOE and includes two methods: init() and shutdown().
   * The Server Object cocreates the SOE and calls the init() method handing it a back reference to the Server Object
   * via the Server Object Helper argument. The Server Object Helper implements a weak reference on the Server Object.
   * The extension can keep a strong reference on the Server Object Helper (for example, in a member variable) but
   * should not keep a strong reference on the Server Object. The log entries are merely informative and completely
   * optional.
   ****************************************************************************************************************************/
  /**
   * init() is called once, when the instance of the SOE is created.
   */
  public void init(IServerObjectHelper soh) throws IOException, AutomationException
  {
    /*
     * An SOE should get the Server Object from the Server Object Helper in order to make any method calls on the
     * Server Object and release the reference after making the method calls.
     */
    this.soHelper = soh;
    this.serverLog = getServerLogger();  
    
    // get the Server Object (SO) this SOE is associated with
    this.mapServer = (MapServer) soHelper.getServerObject();

    // get the Map object from SO
    Map map = (Map) this.mapServer.getMap("Layers");

    // Get index of layer containing bus stations
    int poiLayerIndex = getLayerIndex(map, "PointsOfInterest");
    
    // contruct a feature class for bus stops
    IFeatureLayer poiFL = (IFeatureLayer) mapServer.getLayer("", poiLayerIndex);
    this.poiFC = new FeatureClass(poiFL.getFeatureClass());
  }

  /**
   * shutdown() is called once when the Server Object's context is being shut down and is about to go away.
   */
  public void shutdown() throws IOException, AutomationException
  {
    /*
     * The SOE should release its reference on the Server Object Helper.
     */
    this.poiFC = null;
    this.mapServer = null;
    this.soHelper = null;
    this.serverLog = null;
  }

  /****************************************************************************************************************************
   * IRESTRequestHandler methods: This interface indicates that SOE supports REST. It exposes two methods: handleRESTRequest()
   * and getSchema().
   ****************************************************************************************************************************/
  /**
   * Handles REST request
   */  
  public byte[] handleRESTRequest(String capabilities, String resourceName, 
      String operationName, String operationInput, 
      String outputFormat, String requestProperties, 
      String[] responseProperties) throws IOException, AutomationException
  {
    /*
     * This method handles REST requests by determining whether an operation
     * or resource has been invoked and then forwards the request to
     * appropriate methods.
     */

    try
    {
      // if no operationName is specified send description of specified
      // resource
      if (operationName.length() == 0)
      {
        return getResource(resourceName);
      }
      else
      // invoke REST operation on specified resource
      {
        return invokeRESTOperation(capabilities, resourceName, operationName, operationInput, outputFormat,
            requestProperties, responseProperties);
      }
    }
    catch (Exception e)
    {
      return ServerUtilities.sendError(0, "Exception occurred: " + e.getMessage(), null).getBytes("utf-8");
    }  
  }
  
  /**
   * Returns schema of the REST resource
   */
  public String getSchema() throws IOException, AutomationException
  {
    try
    {
      JSONObject _ServerObjectExt = createResource("POIFinderSOE", "Finds points of interesr such as restaurants and gas stations", false);
      JSONArray _ServerObjectExt_OpArray = new JSONArray();
      _ServerObjectExt_OpArray.put(createOperation("findGasStationByName", "gasStationName", "json"));
      _ServerObjectExt_OpArray.put(createOperation("findRestaurantByName", "restaurantName", "json"));
      _ServerObjectExt.put("operations", _ServerObjectExt_OpArray);
      JSONArray _ServerObjectExt_SubResourceArray = new JSONArray();
      JSONObject _GasStations = createResource("GasStations", "Gas Stations", true);
      _ServerObjectExt_SubResourceArray.put(_GasStations);
      JSONObject _Restaurants = createResource("Restaurants", "Restaurants", true);
      _ServerObjectExt_SubResourceArray.put(_Restaurants);
      _ServerObjectExt.put("resources", _ServerObjectExt_SubResourceArray);
      return _ServerObjectExt.toString();
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
    return null;

  }
  
  /****************************************************************************************************************************
   * SOE Util methods.
   ****************************************************************************************************************************/
  /**
   * Invokes specified REST operation on specified REST resource
   * @param capabilitiesList
   * @param resourceName
   * @param operationName
   * @param operationInput
   * @param outputFormat
   * @param requestProperties
   * @param responseProperties
   * @return
   */
  private byte[] invokeRESTOperation(String capabilitiesList, String resourceName, String operationName,
      String operationInput, String outputFormat, String requestProperties, String[] responseProperties) throws Exception
  {
    byte[] operationOutput = null;

    JSONObject operationInputAsJSON = new JSONObject(operationInput);

    //parse request properties and create a map
    java.util.Map<String, String> requestPropertiesMap = new HashMap<String, String>();
    if (requestProperties != null && requestProperties.length() > 0)
    {
      JSONObject requestPropertiesJSON = new JSONObject(requestProperties);
      Iterator<String> jsonKeys = requestPropertiesJSON.keys();
      while (jsonKeys.hasNext())
      {
        String key = jsonKeys.next();
        requestPropertiesMap.put(key, requestPropertiesJSON.getString(key));
      }
    }

    //create a Map to hold response properties
    java.util.Map<String, String> responsePropertiesMap = new HashMap<String, String>();  
    
    if (resourceName.equalsIgnoreCase("") || resourceName.length() == 0)
    {  
      if (operationName.equalsIgnoreCase("findGasStationByName"))
      {
        operationOutput = findGasStationByName(operationInputAsJSON);
      }
      else if (operationName.equalsIgnoreCase("findRestaurantByName"))
      {
        operationOutput = findRestaurantByName(operationInputAsJSON);
      }
      else
      {
        operationOutput = sendError(0, "Operation " + "\"" + operationName + "\" not supported on sub-resource " + resourceName + ".", 
            new String[]{"no details", " specified"}).getBytes();            
      }
    }
    else if(resourceName.equalsIgnoreCase("GasStations/[0-9]+"))
    {
      int id = Integer.valueOf(resourceName.substring(resourceName.lastIndexOf("/") + 1, resourceName.length())).intValue();
      operationOutput = getGasStation(id).toString().getBytes();
    }
    else if(resourceName.equalsIgnoreCase("Restaurants/[0-9]+"))
    {
      int id = Integer.valueOf(resourceName.substring(resourceName.lastIndexOf("/") + 1, resourceName.length())).intValue();
      operationOutput = getRestaurant(id).toString().getBytes();
    }
    else //if non existent sub-resource specified, report error
    {
      operationOutput = sendError(0, "No sub-resource by name \"" + resourceName + "\" found.", new String[]{""}).getBytes();  
    }      
    
    //convert response properties to String array
    if (!responsePropertiesMap.isEmpty())
    {
      Set<String> keys = responsePropertiesMap.keySet();
      Iterator<String> keysIterator = keys.iterator();
      int i = 0;
      while (keysIterator.hasNext())
      {
        String key = keysIterator.next();
        String value = responsePropertiesMap.get(key);
        responseProperties[i] = key + "=" + value;
        i++;
      }
    }
    
    return operationOutput;  
  }    
  
  /**
   * Returns description of resource specified by resourceName
   * @param resourceName
   * @return byte[]
   */
  private byte[] getResource(String resourceName)
  {    
    if(resourceName.equalsIgnoreCase("") || resourceName.length() == 0)
    {
      return getRootResourceDescription().toString().getBytes();
    }
    else if(resourceName.matches("GasStations/[0-9]+"))
    {
      int id = Integer.valueOf(resourceName.substring(resourceName.lastIndexOf("/") + 1, resourceName.length())).intValue();
      return getGasStation(id).toString().getBytes();
    }
    else if(resourceName.matches("Restaurants/[0-9]+"))
    {
      int id = Integer.valueOf(resourceName.substring(resourceName.lastIndexOf("/") + 1, resourceName.length())).intValue();
      return getRestaurant(id).toString().getBytes();
    }    
    
    return null;
  }  
  
  /**
   * Returns description of the root resource
   * @return description as a JSONObject
   */
  private JSONObject getRootResourceDescription()
  {
    try
    {
      JSONObject rootResource = new JSONObject();
      rootResource.put("Name", "POI Finder SOE");
      rootResource.put("Description", "Gas Stations and Restaurants in Portland using REST SOE");
      rootResource.put("GasStations", getGasStationCollection());
      rootResource.put("Restaurants", getRestaurantCollection());
      
      return rootResource;        
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
    
    return null;
  }      
  
  /**
   * Retrieves Gas Station info using specified id
   * @param id
   * @return
   */
  private JSONObject getGasStation(int id)
  {
    try
    {
      if(id >= 1 || id < this.poiFC.featureCount(null))
      {
        String whereClause = "\"OBJECTID\" = " + id + " AND \"Type\" = 1";
        QueryFilter dirQueryFilter = new QueryFilter();
        dirQueryFilter.setWhereClause(whereClause);

        FeatureCursor featureCursor = new FeatureCursor(this.poiFC.search(dirQueryFilter, false));        
        IFeature feature = featureCursor.nextFeature();
        
        if(feature != null)
        {
          JSONObject busRouteJSON = new JSONObject();
          busRouteJSON.put("id", feature.getValue(0));
          busRouteJSON.put("name", feature.getValue(2));
          busRouteJSON.put("address", feature.getValue(3));
          busRouteJSON.put("city", feature.getValue(4));
          busRouteJSON.put("state", feature.getValue(5));
          busRouteJSON.put("zip", feature.getValue(6));  

          return busRouteJSON;
        }
        else
        {
          JSONObject errorObject = new JSONObject();
          errorObject.put("ErrorMessage", "ERROR: No gas station feature with id = " + id + " found.");
          return errorObject;          
        }
      }
      else
      {
        JSONObject errorObject = new JSONObject();
        errorObject.put("ErrorMessage", "ERROR: Invalid Id provided.");
        return errorObject;
      }
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
    catch (AutomationException e)
    {
      e.printStackTrace();
    }
    catch (UnknownHostException e)
    {
      e.printStackTrace();
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }    
      
    return null;  
  }
    
  /**
   * Retrieves Restaurant info using specified id
   * @param id
   * @return
   */
  private JSONObject getRestaurant(int id)
  {
    try
    {
      if(id >= 1 || id < this.poiFC.featureCount(null))
      {
        String whereClause = "\"OBJECTID\" = " + id + " AND \"Type\" = 4";
        QueryFilter dirQueryFilter = new QueryFilter();
        dirQueryFilter.setWhereClause(whereClause);

        FeatureCursor featureCursor = new FeatureCursor(this.poiFC.search(dirQueryFilter, false));        
        IFeature feature = featureCursor.nextFeature();
        
        if(feature != null)
        {
          JSONObject busRouteJSON = new JSONObject();
          busRouteJSON.put("id", feature.getValue(0));
          busRouteJSON.put("name", feature.getValue(2));
          busRouteJSON.put("address", feature.getValue(3));
          busRouteJSON.put("city", feature.getValue(4));
          busRouteJSON.put("state", feature.getValue(5));
          busRouteJSON.put("zip", feature.getValue(6));  

          return busRouteJSON;
        }
        else
        {
          JSONObject errorObject = new JSONObject();
          errorObject.put("ErrorMessage", "ERROR: No restaurant feature with id = " + id + " found.");
          return errorObject;          
        }
      }
      else
      {
        JSONObject errorObject = new JSONObject();
        errorObject.put("ErrorMessage", "ERROR: Invalid Id provided.");
        return errorObject;
      }
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
    catch (AutomationException e)
    {
      e.printStackTrace();
    }
    catch (UnknownHostException e)
    {
      e.printStackTrace();
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }    
      
    return null;
  }
  
  /**
   * Finds gas station by specified name
   * @param gasStationName
   * @return
   */
  private byte[] findGasStationByName(JSONObject operationInput) throws Exception
  {    
    JSONArray array = null;
    String gasStationName = operationInput.getString("gasStationName");
    
    try
    {      
      String whereClause = "\"Name\" LIKE '%" + gasStationName + "%' AND \"Type\" = 1";     
      array = findPOIs(whereClause);
      
      if(array.length() == 0)
      {        
        JSONObject errorObject = new JSONObject();
        errorObject.put("Warning", "No gas station that contains \"" + gasStationName + "\" in it's name found.");
        array.put(errorObject);          
      }
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
    
    return array.toString().getBytes("utf-8");
  }
  
  /**
   * Finds restaurant by specified name
   * @param restaurantName
   * @return
   */
  private byte[] findRestaurantByName(JSONObject operationInput) throws Exception
  {
    JSONArray array = null;
    String restaurantName = operationInput.getString("restaurantName");
    
    try
    {
      String whereClause = "\"Name\" LIKE '%" + restaurantName + "%' AND \"Type\" = 4";       
      array = findPOIs(whereClause);
      
      if(array.length() == 0)
      {        
        JSONObject errorObject = new JSONObject();
        errorObject.put("Warning", "No restaurant that contains \"" + restaurantName + "\" in it's name found.");
        array.put(errorObject);          
      }
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
    
    return array.toString().getBytes("utf-8");
  }
  
  /**
   * Find points of interest using specified where class
   * @param whereClause
   * @return
   */
  private JSONArray findPOIs(String whereClause)
  {
    JSONArray array = null;
    
    try
    {  
      QueryFilter dirQueryFilter = new QueryFilter();
      dirQueryFilter.setWhereClause(whereClause);
      
      FeatureCursor featureCursor = new FeatureCursor(this.poiFC.search(dirQueryFilter, false));        
      IFeature feature = featureCursor.nextFeature();
      
      array = new JSONArray();
      
      while(feature != null)
      {
        JSONObject json = new JSONObject();
        json.put("id", feature.getValue(0));
        json.put("name", feature.getValue(2));
        json.put("address", feature.getValue(3));
        json.put("city", feature.getValue(4));
        json.put("state", feature.getValue(5));
        json.put("zip", feature.getValue(6));
        
        array.put(json);
        
        feature = featureCursor.nextFeature();
      }
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }
    
    return array;
  }
  
  /**
   * Returns Gas Station collection
   * @return
   */
  private JSONArray getGasStationCollection()
  {
    JSONArray gasStationsArray = new JSONArray();
    for(int i = 1; i < 41; i+=5)
    {
      gasStationsArray.put(getGasStation(i));
    }    
      
    return gasStationsArray;    
  }

  /**
   * Returns Restaurant collection
   * @return
   */
  private JSONArray getRestaurantCollection()
  {
    JSONArray restaurantArray = new JSONArray();
    for(int i = 86; i < 137; i+=5)
    {
      restaurantArray.put(getRestaurant(i));
    }    
      
    return restaurantArray;    
  }
  
  /****************************************************************************************************************************
   * General Util methods.
   ****************************************************************************************************************************/
  /**
   * Creates a REST resource based on specified name, description and collection flag
   * @param name
   * @param description
   * @param isCollection
   * @return REST resource as a JSONObject
   */
  private JSONObject createResource(String name, String description, boolean isCollection)
  {
    try
    {
      JSONObject json = new JSONObject();

      if (name.length() > 0 && name != null)
      {
        json.put("name", name);
      }
      else
      {
        throw new Exception("Resource must have a valid name.");
      }

      json.put("description", description);

      json.put("isCollection", isCollection);

      return json;
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }

    return null;
  }

  /**
   * Creates an operation that can be called on a REST resource, using specified name, parameter list and 
   * output formats list
   * @param operationName
   * @param parameterList
   * @param supportedOutputFormatsList
   * @return Operation as a JSONObject
   */
  private JSONObject createOperation(String operationName, String parameterList, String supportedOutputFormatsList)
  {
    try
    {
      JSONObject operation = new JSONObject();

      if (operationName.length() > 0 && operationName != null)
      {
        operation.put("name", operationName);
      }
      else
      {
        throw new Exception("Operation must have a valid name.");
      }

      // parameters
      if (parameterList.length() > 0 && parameterList != null)
      {
        JSONArray operationParamArray = new JSONArray();
        String[] parameters = parameterList.split(",");
        for (String parameter : parameters)
        {
          operationParamArray.put(parameter.trim());
        }

        operation.put("parameters", operationParamArray);
      }
      else
      {
        throw new Exception(
            "Operation must have parameters. If your operation does not requires params, then please convert it to a sub-resource.");
      }

      // supported Output formats
      if (supportedOutputFormatsList.length() > 0 && supportedOutputFormatsList != null)
      {
        JSONArray outputFormatsArray = new JSONArray();
        String[] outputFormats = supportedOutputFormatsList.split(",");
        for (String outputFormat : outputFormats)
        {
          outputFormatsArray.put(outputFormat.trim());
        }

        operation.put("supportedOutputFormats", outputFormatsArray);
      }
      else
      {
        throw new Exception("Operation must have supported output formats specified");
      }

      return operation;
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }

    return null;
  }

  /**
   * Returns a detailed error thats constructed based on specified code, message and details array. 
   * @param code
   * @param message
   * @param details
   * @return error String
   */
  private String sendError(int code, String message, String[] details)
  {
    /*
     * An json error is sent back to client in the following structure.{ "error": { "code": 400, "message":
     * "Cannot perform query. Invalid query parameters.", "details": ["'time' param is invalid"] }}
     */
    try
    {
      JSONObject errorObject = new JSONObject();

      JSONObject error = new JSONObject();
      error.put("code", code);
      error.put("message", message);

      if (details != null)
      {
        JSONArray detailsArray = new JSONArray();
        for (String detail : details)
        {
          detailsArray.put(detail);
        }

        error.put("details", detailsArray);
      }

      errorObject.put("warning", error);

      return errorObject.toString();
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }

    return null;
  }

  /**
   * Retrieves ID of a layer
   * 
   * @param mapServer
   * @param layerName
   * @return
   */
  private int getLayerIndex(Map map, String layerName) throws IOException, AutomationException
  {
    int layerID = -1;

    for (int i = 0; i < map.getLayerCount(); i++)
    {
      String name = map.getLayer(i).getName();
      if (layerName.equalsIgnoreCase(name))
      {
        layerID = i;
        break;
      }
    }

    if (layerID < 0)
    {
      serverLog.addMessage(4, 8000, "Could not find layer " + layerName + " in " + map.getName());
    }

    return layerID;
  }
}