| <?xml version="1.0" encoding="UTF-8"?> |
| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to you 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. |
| --> |
| |
| <document id="query-servlet"> |
| <properties> |
| <title>QueryServlet</title> |
| </properties> |
| |
| <body> |
| <p> |
| As discussed in the previous section, Pivot provides a set of classes for interacting |
| with HTTP-based REST services, which Pivot calls "web queries". Pivot also provides an |
| abstract base class named <tt>org.apache.pivot.web.server.QueryServlet</tt> that helps |
| to facilitate implementation of such services. |
| </p> |
| |
| <p> |
| The following example shows a Pivot client application that interacts with a REST |
| service for managing expense data. The web service is implemented using |
| <tt>QueryServlet</tt> and is discussed below: |
| </p> |
| |
| <p> |
| <em>NOTE</em> This application must be run via a locally-deployed WAR file. It will not |
| work in the online tutorial. |
| </p> |
| |
| <application class="org.apache.pivot.tutorials.webqueries.Expenses" |
| width="480" height="280"> |
| <libraries> |
| <library>core</library> |
| <library>web</library> |
| <library>wtk</library> |
| <library>wtk-terra</library> |
| <library>tutorials</library> |
| </libraries> |
| </application> |
| |
| <h3>QueryServlet</h3> |
| |
| <p> |
| The <tt>QueryServlet</tt> class extends <tt>javax.servlet.http.HttpServlet</tt> and |
| provides overloaded versions of the base HTTP handler methods that make them easier to |
| work with in a REST-oriented manner: |
| </p> |
| |
| <ul> |
| <li><tt>Object doGet(Path path)</tt></li> |
| <li><tt>URL doPost(Path path, Object value)</tt></li> |
| <li><tt>boolean doPut(Path path, Object value)</tt></li> |
| <li><tt>void doDelete(Path path)</tt></li> |
| </ul> |
| |
| <p> |
| Each method takes an instance of <tt>QueryServlet.Path</tt> that represents the path to |
| the resource being accessed, relative to the location of the servlet itself. |
| <tt>Path</tt> is a sequence type that allows a caller to access the components of the |
| path via numeric index. For example, if the query servlet is mapped to the |
| "/my_service/*" URL pattern, given the following URL: |
| </p> |
| |
| <pre>http://pivot.apache.org/my_service/foo/bar</pre> |
| |
| <p> |
| the <tt>path</tt> argument would contain the values "foo" and "bar", accessible via |
| indices 0 and 1, respectively. |
| </p> |
| |
| <h4>Serializers</h4> |
| |
| <p> |
| Unlike the base <tt>HttpServlet</tt> class, <tt>QueryServlet</tt> operates on arbitrary |
| Java types rather than HTTP request and response objects. This allows developers to |
| focus on the resources managed by the service rather than the lower-level details of |
| the HTTP protocol. |
| </p> |
| |
| <p> |
| <tt>QueryServlet</tt> uses a serializer (an implementation of the |
| <tt>org.apache.pivot.serialization.Serializer</tt> interface) to determine how to |
| serialize the data sent to and returned from the servlet. The serializer used for a |
| given HTTP request is determined by the return value of the abstract |
| <tt>createSerializer()</tt> method. This method is called by <tt>QueryServlet</tt> |
| prior to invoking the actual HTTP handler method. The example servlet uses an instance |
| of <tt>org.apache.pivot.json.JSONSerializer</tt>, which supports reading and writing of |
| JSON data. Pivot provides a number of additional serializers supporting XML, CSV, and |
| Java serialization, among others, and service implementations are free to define their |
| own custom serializers as well. |
| </p> |
| |
| <h4>Exceptions</h4> |
| |
| <p> |
| Each handler method declares that it may throw an instance of |
| <tt>org.apache.pivot.web.QueryException</tt>. This exception encapsulates an HTTP error |
| response. It takes an integer value representing the response code as a constructor |
| argument (the <tt>org.apache.pivot.web.Query.Status</tt> class defines a number of |
| constants for status codes commonly used in REST responses). The web query client API, |
| discussed in the previous section, effectively re-throws these exceptions, allowing the |
| client to handle an error response returned by the server as if the exception was |
| generated locally. |
| </p> |
| |
| <h4>Query String Parameters and HTTP Headers</h4> |
| |
| <p> |
| Though it is not shown in this example, query servlet implementations can also access |
| the query string arguments and HTTP headers included in the HTTP request, as well as |
| control the headers sent back with the response. Query string parameters are accessible |
| via the <tt>getParameters()</tt> method of <tt>QueryServlet</tt>, and the |
| request/response headers can be accessed via <tt>getRequestHeaders()</tt> and |
| <tt>getResponseHeaders()</tt>, respectively. All three methods return an instance of |
| <tt>org.apache.pivot.web.QueryDictionary</tt>, which allows the caller to manipulate |
| these collections via <tt>get()</tt>, <tt>put()</tt>, and <tt>remove()</tt> methods. |
| </p> |
| |
| <h3>ExpenseServlet</h3> |
| |
| <p> |
| The following listing contains the full source code for <tt>ExpenseServlet</tt>, which |
| provides the implementation for the REST service used in this example. The |
| implementation of each method is discussed in more detail below: |
| </p> |
| |
| <source type="java" location="org/apache/pivot/tutorials/webqueries/server/ExpenseServlet.java"> |
| <![CDATA[ |
| package org.apache.pivot.tutorials.webqueries.server; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| |
| import javax.servlet.ServletException; |
| |
| import org.apache.pivot.collections.HashMap; |
| import org.apache.pivot.collections.List; |
| import org.apache.pivot.json.JSONSerializer; |
| import org.apache.pivot.serialization.CSVSerializer; |
| import org.apache.pivot.serialization.SerializationException; |
| import org.apache.pivot.serialization.Serializer; |
| import org.apache.pivot.web.Query; |
| import org.apache.pivot.web.QueryException; |
| import org.apache.pivot.web.server.QueryServlet; |
| |
| /** |
| * Servlet that implements expense management web service. |
| */ |
| public class ExpenseServlet extends QueryServlet { |
| private static final long serialVersionUID = 0; |
| |
| private List<Expense> expenses = null; |
| private HashMap<Integer, Expense> expenseMap = new HashMap<Integer, Expense>(); |
| |
| private static int nextID = 0; |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public void init() throws ServletException { |
| CSVSerializer csvSerializer = new CSVSerializer(); |
| csvSerializer.getKeys().add("date"); |
| csvSerializer.getKeys().add("type"); |
| csvSerializer.getKeys().add("amount"); |
| csvSerializer.getKeys().add("description"); |
| csvSerializer.setItemClass(Expense.class); |
| |
| // Load the initial expense data |
| InputStream inputStream = ExpenseServlet.class.getResourceAsStream("expenses.csv"); |
| |
| try { |
| expenses = (List<Expense>)csvSerializer.readObject(inputStream); |
| } catch (IOException exception) { |
| throw new ServletException(exception); |
| } catch (SerializationException exception) { |
| throw new ServletException(exception); |
| } |
| |
| // Index the initial expenses |
| for (Expense expense : expenses) { |
| int id = nextID++; |
| expense.setID(id); |
| expenseMap.put(id, expense); |
| } |
| } |
| |
| @Override |
| protected Object doGet(Path path) throws QueryException { |
| Object value; |
| |
| if (path.getLength() == 0) { |
| value = expenses; |
| } else { |
| // Get the ID of the expense to retrieve from the path |
| int id = Integer.parseInt(path.get(0)); |
| |
| // Get the expense data from the map |
| synchronized (this) { |
| value = expenseMap.get(id); |
| } |
| |
| if (value == null) { |
| throw new QueryException(Query.Status.NOT_FOUND); |
| } |
| } |
| |
| return value; |
| } |
| |
| @Override |
| protected URL doPost(Path path, Object value) throws QueryException { |
| if (value == null) { |
| throw new QueryException(Query.Status.BAD_REQUEST); |
| } |
| |
| Expense expense = (Expense)value; |
| |
| // Add the expense to the list/map |
| int id; |
| synchronized (this) { |
| id = nextID++; |
| expense.setID(id); |
| expenses.add(expense); |
| expenseMap.put(id, expense); |
| } |
| |
| // Return the location of the newly-created resource |
| URL location = getLocation(); |
| try { |
| location = new URL(location, Integer.toString(id)); |
| } catch (MalformedURLException exception) { |
| throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR); |
| } |
| |
| return location; |
| } |
| |
| @Override |
| protected boolean doPut(Path path, Object value) throws QueryException { |
| if (path.getLength() == 0 |
| || value == null) { |
| throw new QueryException(Query.Status.BAD_REQUEST); |
| } |
| |
| // Get the ID of the expense to retrieve from the path |
| int id = Integer.parseInt(path.get(0)); |
| |
| // Create the new expense and bind the data to it |
| Expense expense = (Expense)value; |
| expense.setID(id); |
| |
| // Update the list/map |
| Expense previousExpense; |
| synchronized (this) { |
| previousExpense = expenseMap.put(id, expense); |
| expenses.remove(previousExpense); |
| expenses.add(expense); |
| } |
| |
| return (previousExpense == null); |
| } |
| |
| @Override |
| protected void doDelete(Path path) throws QueryException { |
| if (path.getLength() == 0) { |
| throw new QueryException(Query.Status.BAD_REQUEST); |
| } |
| |
| // Get the ID of the expense to retrieve from the path |
| int id = Integer.parseInt(path.get(0)); |
| |
| // Update the list/map |
| Expense expense; |
| synchronized (this) { |
| expense = expenseMap.remove(id); |
| expenses.remove(expense); |
| } |
| |
| if (expense == null) { |
| throw new QueryException(Query.Status.NOT_FOUND); |
| } |
| } |
| |
| @Override |
| protected Serializer<?> createSerializer(Path path) throws QueryException { |
| return new JSONSerializer(Expense.class); |
| } |
| } |
| ]]> |
| </source> |
| |
| <h3>init()</h3> |
| |
| <p> |
| The <tt>init()</tt> method, which is defined by <tt>QueryServlet</tt>'s base class |
| <tt>HttpServlet</tt>, is called when a servlet is first created by a servlet container. |
| <tt>ExpenseServlet</tt>'s implementation of <tt>init()</tt> is as follows: |
| </p> |
| |
| <source type="java"> |
| <![CDATA[ |
| public void init() throws ServletException { |
| CSVSerializer csvSerializer = new CSVSerializer(); |
| csvSerializer.getKeys().add("date"); |
| csvSerializer.getKeys().add("type"); |
| csvSerializer.getKeys().add("amount"); |
| csvSerializer.getKeys().add("description"); |
| csvSerializer.setItemClass(Expense.class); |
| |
| // Load the initial expense data |
| InputStream inputStream = ExpenseServlet.class.getResourceAsStream("expenses.csv"); |
| |
| try { |
| expenses = (List<Expense>)csvSerializer.readObject(inputStream); |
| } catch (IOException exception) { |
| throw new ServletException(exception); |
| } catch (SerializationException exception) { |
| throw new ServletException(exception); |
| } |
| |
| // Index the initial expenses |
| for (Expense expense : expenses) { |
| int id = nextID++; |
| expense.setID(id); |
| expenseMap.put(id, expense); |
| } |
| } |
| ]]> |
| </source> |
| |
| <p> |
| It loads an initial list of expenses using an instance of <tt>CSVSerializer</tt>. The |
| contents of the CSV file are as follows: |
| </p> |
| |
| <pre> |
| 2010-03-28, Travel, 1286.90, Ticket #145-XX-71903-09 |
| 2010-03-28, Meals, 34.12, Took client out |
| 2010-03-31, Meals, 27.00, |
| 2010-04-01, Meals, 12.55, |
| 2010-04-02, Meals, 18.86, |
| 2010-04-02, Parking, 30.00, Cambridge Center parking |
| 2010-04-03, Meals, 20.72, |
| 2010-04-06, Travel, 529.00, Marriott reservation #DF-9982-BRN |
| </pre> |
| |
| <p> |
| Due to the call to <tt>setItemClass()</tt>, the rows in the CSV file are deserialized |
| as instances of <tt>org.apache.pivot.tutorials.webqueries.server.Expense</tt>, a Java |
| Bean class that is defined as follows: |
| </p> |
| |
| <source type="java" location="org/apache/pivot/tutorials/webqueries/server/Expense.java"> |
| <![CDATA[ |
| package org.apache.pivot.tutorials.webqueries.server; |
| |
| public class Expense { |
| private Integer id = -1; |
| private String date = null; |
| private String type = null; |
| private Double amount = 0d; |
| private String description = null; |
| |
| public Integer getID() { |
| return id; |
| } |
| |
| public Integer getId() { |
| return getID(); |
| } |
| |
| public void setID(Integer id) { |
| this.id = id; |
| } |
| |
| public void setId(Integer id) { |
| setID(id); |
| } |
| |
| public String getDate() { |
| return date; |
| } |
| |
| public void setDate(String date) { |
| this.date = date; |
| } |
| |
| public String getType() { |
| return type; |
| } |
| |
| public void setType(String type) { |
| this.type = type; |
| } |
| |
| public Double getAmount() { |
| return amount; |
| } |
| |
| public void setAmount(Double amount) { |
| this.amount = amount; |
| } |
| |
| public final void setAmount(String amount) { |
| setAmount(Double.parseDouble(amount)); |
| } |
| |
| public String getDescription() { |
| return description; |
| } |
| |
| public void setDescription(String description) { |
| this.description = description; |
| } |
| } |
| ]]> |
| </source> |
| |
| <p> |
| After the list of expenses has been loaded, <tt>init()</tt> iterates over the list, |
| assigns each expense an ID, and adds it to a map. This collection-based approach is |
| sufficient for a tutorial example; a real application would most likely use a |
| relational database to manage the expense data. |
| </p> |
| |
| <h3>doGet()</h3> |
| |
| <p> |
| <tt>doGet()</tt> is used to handle an HTTP GET request. It returns an object |
| representing the resource at a given path. The <tt>doGet()</tt> method in the example |
| servlet is defined as follows: |
| </p> |
| |
| <source type="java"> |
| <![CDATA[ |
| protected Object doGet(Path path) throws QueryException { |
| Object value; |
| |
| if (path.getLength() == 0) { |
| value = expenses; |
| } else { |
| // Get the ID of the expense to retrieve from the path |
| int id = Integer.parseInt(path.get(0)); |
| |
| // Get the expense data from the map |
| synchronized (this) { |
| value = expenseMap.get(id); |
| } |
| |
| if (value == null) { |
| throw new QueryException(Query.Status.NOT_FOUND); |
| } |
| } |
| |
| return value; |
| } |
| ]]> |
| </source> |
| |
| <p> |
| If the request does not contain a path, the method returns the list of all expenses. |
| Otherwise, it attemps to look up and return the requested expense by its ID. If the |
| expense is not found, an HTTP 404 ("Not Found") error is returned to the caller via the |
| thrown <tt>QueryException</tt>; otherwise, the expense is returned along with the |
| default HTTP 200 ("OK") status code. The bean value is converted to JSON format by the |
| <tt>JSONSerializer</tt> instance returned by <tt>createSerializer()</tt>. |
| </p> |
| |
| <h3>doPost()</h3> |
| |
| <p> |
| <tt>doPost()</tt> is used to handle an HTTP POST request. It is primarily used to |
| create a new resource on the server, but can also be used to execute arbitrary |
| server-side actions. |
| </p> |
| |
| <p> |
| When a resource is created, <tt>doPost()</tt> returns a URL representing the location |
| of the new resource. Consistent with the HTTP specification, this value is returned in |
| the "Location" response header along with an HTTP status code of 201 ("Created"). If a |
| POST request does not result in the creation of a resource, <tt>doPost()</tt> can |
| return <tt>null</tt>, which is translated by <tt>QueryServlet</tt> to an HTTP response |
| of 204 ("No Content") and no corresponding "Location" header. |
| </p> |
| |
| <p> |
| The <tt>doPost()</tt> method in the example looks like this: |
| </p> |
| |
| <source type="java"> |
| <![CDATA[ |
| protected URL doPost(Path path, Object value) throws QueryException { |
| if (value == null) { |
| throw new QueryException(Query.Status.BAD_REQUEST); |
| } |
| |
| Expense expense = (Expense)value; |
| |
| // Add the expense to the list/map |
| int id; |
| synchronized (this) { |
| id = nextID++; |
| expense.setID(id); |
| expenses.add(expense); |
| expenseMap.put(id, expense); |
| } |
| |
| // Return the location of the newly-created resource |
| URL location = getLocation(); |
| try { |
| location = new URL(location, Integer.toString(id)); |
| } catch (MalformedURLException exception) { |
| throw new QueryException(Query.Status.INTERNAL_SERVER_ERROR); |
| } |
| |
| return location; |
| } |
| ]]> |
| </source> |
| |
| <p> |
| The first thing the method does is ensure that the request is valid. If the caller has |
| not provided a value in the body of the request, HTTP 400 ("Bad Request") is returned. |
| Otherwise, it assigns the expense an ID and adds it to the list and map. |
| </p> |
| |
| <p> |
| Finally, it returns the location of the new expense resource. The location value is |
| generated simply by appending the name of the temp file to the location of the servlet, |
| obtained by a call to <tt>QueryServlet#getLocation()</tt>. |
| </p> |
| |
| <h3>doPut()</h3> |
| |
| <p> |
| <tt>doPut()</tt> handles an HTTP PUT request. It is often used to update an existing |
| resource, but can also be used to create a new resource. The return value of |
| <tt>doPut()</tt> is a boolean flag indicating whether or not a resource was created. |
| If <tt>true</tt>, HTTP 201 is returned to the caller; otherwise, HTTP 204 is |
| returned. |
| </p> |
| |
| <p> |
| <tt>ExpenseServlet</tt>'s implementation of <tt>doPut()</tt> is as follows: |
| </p> |
| |
| <source type="java"> |
| <![CDATA[ |
| protected boolean doPut(Path path, Object value) throws QueryException { |
| if (path.getLength() == 0 |
| || value == null) { |
| throw new QueryException(Query.Status.BAD_REQUEST); |
| } |
| |
| // Get the ID of the expense to retrieve from the path |
| int id = Integer.parseInt(path.get(0)); |
| |
| // Create the new expense and bind the data to it |
| Expense expense = (Expense)value; |
| expense.setID(id); |
| |
| // Update the list/map |
| Expense previousExpense; |
| synchronized (this) { |
| previousExpense = expenseMap.put(id, expense); |
| expenses.remove(previousExpense); |
| expenses.add(expense); |
| } |
| |
| return (previousExpense == null); |
| } |
| ]]> |
| </source> |
| |
| <p> |
| Like <tt>doPost()</tt>, it first validates the format of the request. In addition to a |
| body, <tt>doPut()</tt> also requires a path component to identify the resource to be |
| updated. A real expense service might want to verify that the requested resource exists |
| before proceeding; however, the example service simply interprets an unused ID as a |
| request to create a new resource. Consistent with the API, it returns <tt>true</tt> if |
| a resource was created and <tt>false</tt> otherwise. |
| </p> |
| |
| <h3>doDelete()</h3> |
| |
| <p> |
| <tt>doDelete()</tt> handles an HTTP DELETE request. When successful, it simply |
| deletes the resource specified by the path and returns HTTP 204. The source code for |
| this method is shown below: |
| </p> |
| |
| <source type="java"> |
| <![CDATA[ |
| protected void doDelete(Path path) throws QueryException { |
| if (path.getLength() == 0) { |
| throw new QueryException(Query.Status.BAD_REQUEST); |
| } |
| |
| // Get the ID of the expense to retrieve from the path |
| int id = Integer.parseInt(path.get(0)); |
| |
| // Update the list/map |
| Expense expense; |
| synchronized (this) { |
| expense = expenseMap.remove(id); |
| expenses.remove(expense); |
| } |
| |
| if (expense == null) { |
| throw new QueryException(Query.Status.NOT_FOUND); |
| } |
| } |
| ]]> |
| </source> |
| |
| <p> |
| Like the other methods, the request is first validated; then, if the expense exists, |
| it is deleted. Otherwise, HTTP 404 is returned. |
| </p> |
| |
| <h3>The Expenses Application</h3> |
| |
| <p> |
| The Expenses client application allows a user to interact with the web service. It is |
| not described in this section, but builds on concepts discussed in earlier sections. |
| The source code is available in the Pivot source distribution under the "tutorials" |
| project. |
| </p> |
| </body> |
| </document> |