Java tutorial
/* * RESTx: Sane, simple and effective data publishing and integration. * * Copyright (C) 2010 MuleSoft Inc. http://www.mulesoft.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.mulesoft.restx.clientapi; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; /** * Used by the client to encapsulate all communication with the server. When created, * some meta information from the server is read and stored. This object also serves * as a cache for often accessed information. */ public class RestxServer { // The well-known URIs where we can find specific server information protected static final String CODE_URI_KEY = "code"; protected static final String DOC_URI_KEY = "doc"; protected static final String NAME_KEY = "name"; protected static final String RESOURCE_URI_KEY = "resource"; protected static final String SPECIALIZED_URI_KEY = "specialized code"; protected static final String STATIC_URI_KEY = "static"; protected static final String VERSION_KEY = "version"; protected static final String META_URI = "/"; protected static final String DESCURI_DESC_KEY = "desc"; protected static final String DESCURI_URI_KEY = "uri"; protected static final String[] REQUIRED_KEYS = { CODE_URI_KEY, DOC_URI_KEY, NAME_KEY, RESOURCE_URI_KEY, STATIC_URI_KEY, VERSION_KEY, SPECIALIZED_URI_KEY }; protected HashMap<String, String> DEFAULT_REQ_HEADERS; protected String serverUri; protected String componentUri; protected String docUri; protected String docRoot; protected String name; protected String resourceUri; protected String specializedUri; protected String staticUri; protected String doc; protected String version; protected Map<String, DescUriHolder> resources; protected Map<String, DescUriHolder> components; protected Map<String, DescUriHolder> specializedComponents; /** * Used to send requests to the server. The right headers are assembled and a * suitable HTTP method will be selected if not specified by the caller. * * @param url The URL on the server to which the request should be sent. Since * this relies on an established server connection, the URL here is * just the path component of the URL (including possible query * parameters), starting with '/'. * @param data Any data we want to send with the request (in case of PUT or * POST). If data is specified, but no method is given, then the * method will default to POST. If a method is specified as well then * it must be POST or PUT. If no data should be sent then this should * be 'null'. * @param method The desired HTTP method of the request. * @param status The expected status. If the HTTP status from the server is * different than the expected status, an exception will be thrown. If * no status check should be performed then set this to 'null'. * @param headers A {@link HashMap<String, String>} in which any additional * request headers should be specified. For example: { "Accept" : * "application/json" }. The hash map will not be modified by this * method. * @return A {@link HttpResult} object with status and data that was returned by * the server. * @throws RestxClientException */ public HttpResult send(String url, String data, HttpMethod method, Integer status, HashMap<String, String> headers) throws RestxClientException { // Set default values for the method if nothing was specified. Depends on // whether we want to send data or not. if (method == null) { if (data == null) { method = HttpMethod.GET; } else { method = HttpMethod.POST; } } // Combine default headers with any additional headers if (headers == null) { headers = DEFAULT_REQ_HEADERS; } else { final HashMap<String, String> hm = new HashMap<String, String>(headers); for (final String name : DEFAULT_REQ_HEADERS.keySet()) { hm.put(name, DEFAULT_REQ_HEADERS.get(name)); } headers = hm; } URL fullUrl = null; HttpURLConnection conn = null; try { if (!url.startsWith("/")) { url = "/" + url; } fullUrl = new URL(serverUri + url); conn = (HttpURLConnection) fullUrl.openConnection(); // Set the request headers for (final Entry<String, String> header : headers.entrySet()) { conn.setRequestProperty(header.getKey(), header.getValue()); } // Set the request method conn.setRequestMethod(HttpMethod.toString(method)); // Send the message body if (data != null) { conn.setDoOutput(true); final OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); try { wr.write(data); } finally { wr.close(); } } // Get the response final BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; final StringBuffer buf = new StringBuffer(); while ((line = rd.readLine()) != null) { // For now we do nothing particularly efficient. We just assemble all // the data we get into a big string. buf.append(line); } rd.close(); final int respStatus = conn.getResponseCode(); if (status != null) { if (status != respStatus) { if (buf.length() > 256) { buf.delete(256, buf.length()); } throw new RestxClientException("Status code " + status + " was expected for request to '" + fullUrl + "'. Instead we received " + respStatus + " " + buf); } } return new HttpResult(conn.getResponseCode(), buf.toString()); } catch (final IOException e) { if (conn != null) { // This exception was thrown after we started to connect to the // server int code; String msg; try { // We may even have an initialised response already, in which // case // we are passing that information back to the caller. code = conn.getResponseCode(); msg = conn.getResponseMessage(); } catch (final IOException e1) { // The problem occurred before the response status in the HTTP // connection was initialised. code = HttpURLConnection.HTTP_INTERNAL_ERROR; msg = e.getMessage(); } if (status != null) { if (status != code) { throw new RestxClientException("Status code " + status + " was expected for request to '" + fullUrl + "'. Instead we received " + code); } } return new HttpResult(code, msg); } // The exception was thrown before we even initialised our connection throw new RestxClientException("Cannot connect with URI '" + fullUrl + "': " + e.getMessage()); } } /** * Serialize a complex object to JSON. We should be able to just use * {@link JSONObject} for all kinds of objects. However, it does not handle * strings, numbers or booleans, only maps or arrays. Therefore, we deal with * those manually. * * @param obj The object to be serialized. The object has to be a string, number * or boolean, or a HashMap and/or ArrayList consisting of those basic * types or further HashMaps and ArrayLists. * @return The JSON string representation of the object. * @throws RestxClientException */ public static String jsonSerialize(Object obj) throws RestxClientException { try { final Class<?> oclass = obj.getClass(); if (oclass == Map.class) { // The JSON library offers a specific class for maps... return (new JSONObject(obj)).toString(); } else if (oclass == Collection.class) { // ... and another for arrays. return (new JSONArray(obj)).toString(); } else { // But all other (normal) types are different. For some reason // the JSON library doesn't seem to handle this well. // So, we solve the issue by putting this other type of object // into a list, use JSONArray(), convert to string and strip off // the leading and trailing '[' and ']'. Then we have the proper // JSON-stype string representation of that type, which we can // return. This is extremely kludgy. One of these days we might // final List<Object> al = new ArrayList<Object>(); al.add(obj); final String buf = (new JSONArray(al)).toString(); return buf.substring(1, buf.length() - 1); } } catch (final JSONException e) { throw new RestxClientException("Could not serialize data: " + e.getMessage()); } } /** * Transcribes a JSONObject into a HashMap. Just a small helper method. * * @param obj Instance of {@link JSONObject}. * @return A string representing the JSON serialization. * @throws JSONException */ protected static Map<String, Object> jsonObjectTranscribe(JSONObject obj) throws JSONException { final Map<String, Object> d = new HashMap<String, Object>(); final String[] nameArray = JSONObject.getNames(obj); if (nameArray != null) { for (final String name : JSONObject.getNames(obj)) { Object o = obj.get(name); if (o.getClass() == JSONArray.class) { o = jsonListTranscribe((JSONArray) o); } else if (o.getClass() == JSONObject.class) { o = jsonObjectTranscribe((JSONObject) o); } d.put(name, o); } } return d; } /** * Transcribes a JSONArray object into an ArrayList object. Just a small helper * method. * * @param arr A {@link JSONArray} instance. * @return A string representing the JSON serialization. * @throws JSONException */ protected static List<Object> jsonListTranscribe(JSONArray arr) throws JSONException { final List<Object> l = new ArrayList<Object>(); for (int i = 0; i < arr.length(); ++i) { Object o = arr.get(i); if (o.getClass() == JSONArray.class) { o = jsonListTranscribe((JSONArray) o); } else if (o.getClass() == JSONObject.class) { o = jsonObjectTranscribe((JSONObject) o); } l.add(o); } return l; } /** * Deserializes a JSON string into a complex object. * * @param str A JSON string, representing a complex object. * @return A complex object, which will be a string, number or boolean, or a * HashMap or ArrayList containing any of the other types. * @throws RestxClientException */ public static Object jsonDeserialize(String str) throws RestxClientException { try { final JSONTokener t = new JSONTokener(str); Object v = t.nextValue(); if (v.getClass() == JSONArray.class) { v = jsonListTranscribe((JSONArray) v); } else if (v.getClass() == JSONObject.class) { v = jsonObjectTranscribe((JSONObject) v); } return v; } catch (final JSONException e) { throw new RestxClientException("Could not de-serialize data: " + e.getMessage()); } } /** * Send and receive complex, JSON serialized objects. This is a wrapper around * {@link send}, which serializes complex objects to JSON strings and assume that * the response can equally be de-serialized from JSON. We only decode the * response as JSON if we did not get an error back. * * @param url The URL on the server to which the request should be sent. Since * this relies on an established server connection, the URL here is * just the path component of the URL (including possible query * parameters), starting with '/'. * @param data Any data we want to send with the request (in case of PUT or * POST). If data is specified, but no method is given, then the * method will default to POST. If a method is specified as well then * it must be POST or PUT. If no data should be sent then this should * be 'null'. This is assumed to be a complex object, which will be * JSON serialized before sending. * @param method The desired HTTP method of the request. * @param status The expected status. If the HTTP status from the server is * different than the expected status, an exception will be thrown. If * no status check should be performed then set this to 'null'. * @param headers A {@link HashMap<String, String>} in which any additional * request headers should be specified. For example: { "Accept" : * "application/json" }. The hash map will not be modified by this * method. * @return A {@link HttpResult} object with status and data that was returned by * the server. The data is a complex object, which was de-serialized from * the JSON string that was received from the server. * @throws RestxClientException */ public HttpResult jsonSend(String url, Object data, HttpMethod method, Integer status, HashMap<String, String> headers) throws RestxClientException { String dataStr; // We need to add a few custom headers. Since we don't want to // modify the header map that was passed to us (the caller might // want to use it a few more times), we will make a copy and add // our stuff into that. HashMap<String, String> newHeaders = null; if (headers == null) { newHeaders = new HashMap<String, String>(); } else { newHeaders = new HashMap<String, String>(headers); } newHeaders.put("Accept", "application/json"); if (data != null) { // Serialise data via JSON dataStr = jsonSerialize(data); // Tell the recipient about our content type. We make a new newHeaders.put("Content-type", "application/json"); } else { dataStr = null; } final HttpResult res = send(url, dataStr, method, status, headers); if (res.status < 300) { res.data = jsonDeserialize((String) res.data); // / JSON de-serialized } return res; } /** * Returns description and URI for each resource or component. An overview of * resources and components can be had via the same sort of structure: A * dictionary that maps their names to a brief summary, consisting of description * and URI for each resource or component. * * @param uri The URI from which to retrieve this information. * @return A map consisting of DescUriHolder objects. * @throws RestxClientException */ protected Map<String, DescUriHolder> getDescUriMap(String uri) throws RestxClientException { final HttpResult res = jsonSend(uri, null, null, 200, null); @SuppressWarnings("unchecked") final Map<String, Map<String, String>> namedResources = (Map<String, Map<String, String>>) res.data; // Store the dictionary we get from the server in a hash map // that uses the DescUriHolder class. final HashMap<String, DescUriHolder> namedDuhs = new HashMap<String, DescUriHolder>(); for (final Entry<String, Map<String, String>> namedResource : namedResources.entrySet()) { final Map<String, String> elem = namedResource.getValue(); final DescUriHolder duh = new DescUriHolder(elem.get(DESCURI_DESC_KEY), elem.get(DESCURI_URI_KEY)); namedDuhs.put(namedResource.getKey(), duh); } return namedDuhs; } /******************************************************************* * Public server interface *******************************************************************/ /** * Useful utility method, which checks whether a map contains all required keys. * Throws an exception if not all required keys are present. * * @param hm Map to check for required keys. * @param requiredKeys List of required keys. * @throws RestxClientException */ public static void checkKeyset(Map<?, ?> hm, String[] requiredKeys) throws RestxClientException { for (final String name : requiredKeys) { if (!hm.containsKey(name)) { throw new RestxClientException("Missing expected key '" + name + "."); } } } /** * Create a new client-side representation of a RestxServer. A request is sent * for the server's meta data information. Some basic sanity checking is * performed on the received data. * * @param serverUri The absolute URI at which the server can be found. For * example: "http://localhost:8001". * @throws MalformedURLException * @throws RestxClientException */ public RestxServer(String serverUri) throws MalformedURLException, RestxClientException { // Don't need any trailing '/' int i = serverUri.length() - 1; while (i > 0 && serverUri.charAt(i) == '/') { --i; } if (i > 0) { serverUri = serverUri.substring(0, i + 1); } else { throw new MalformedURLException(); } this.serverUri = serverUri; DEFAULT_REQ_HEADERS = new HashMap<String, String>(); DEFAULT_REQ_HEADERS.put("Accept", "application/json"); final URL url = new URL(serverUri); // Some sanity checking on the URI. this.serverUri = url.getProtocol() + "://" + url.getHost(); if (url.getPort() > -1) { this.serverUri += ":" + Integer.toString(url.getPort()); } if (!url.getPath().isEmpty()) { docRoot = url.getPath(); } else { docRoot = ""; } if (!url.getProtocol().equals("http")) { throw new RestxClientException("Only 'http' schema is currently supported."); } // Receive server meta data final HttpResult res = jsonSend(docRoot + META_URI, null, HttpMethod.GET, 200, null); @SuppressWarnings("unchecked") final Map<String, String> hm = (Map<String, String>) res.data; // Sanity check on received information try { checkKeyset(hm, REQUIRED_KEYS); } catch (final RestxClientException e) { throw new RestxClientException("Server error: Malformed server meta data: " + e.getMessage()); } // Store the meta data for later use try { componentUri = hm.get(CODE_URI_KEY); docUri = hm.get(DOC_URI_KEY); name = hm.get(NAME_KEY); resourceUri = hm.get(RESOURCE_URI_KEY); specializedUri = hm.get(SPECIALIZED_URI_KEY); staticUri = hm.get(STATIC_URI_KEY); version = hm.get(VERSION_KEY); } catch (final Exception e) { throw new RestxClientException("Malformed server meta data: " + e.getMessage()); } doc = null; resources = null; components = null; } /** * Sends the request to create a new resource on the server. Clients don't use * this method, but instead create a resource through a * {@link RestxResourceTemplate} object. * * @param uri The full URI (starting with a '/') of the component. * @param rdict The dictionary with all required parameters for the resource * creation. * @return An HttpResult object, containing the response from the server. * @throws RestxClientException */ @SuppressWarnings("unchecked") protected Map<String, Object> createResource(String uri, Object rdict) throws RestxClientException { final HttpResult res = jsonSend(uri, rdict, null, 201, null); return (Map<String, Object>) res.data; } /** * Return the URI of the server to which we are connected. * * @return The full server URI, as it was specified when the RestxServer object * was created. */ public String getServerUri() { return serverUri; } /** * Return the version string of the server to which we are connected. * * @return The version string of the server. */ public String getServerVersion() { return version; } /** * Returns the name of the server. The 'name' is a token that was returned by the * server when we connected to it for the first time. Each server can be * configured with its own name. However, the URI of the server should actually * be sufficient to differentiate amongst them. * * @return Name of the server. */ public String getServerName() { return name; } /** * Return the doc page for the server. If we don't have a copy of it already then * we issue a request to the server to retrieve the doc information. * * @return A string containing the server documentation. * @throws RestxClientException */ public String getServerDoc() throws RestxClientException { if (doc == null) { final HttpResult res = jsonSend(docUri, null, null, 200, null); doc = (String) res.data; } return doc; } /** * Return high level information about all resources currently known on the * server. This retrieves a map from the server, which contains description and * URI for each resource. * * @return Map with information about each resource. * @throws RestxClientException */ public Map<String, DescUriHolder> getAllResourceNamesPlus() throws RestxClientException { resources = getDescUriMap(resourceUri); return resources; } /** * Return the names of all resources currently known on the server. * * @return Names of all resources. * @throws RestxClientException */ public String[] getAllResourceNames() throws RestxClientException { // Refresh the resource list. getAllResourceNamesPlus(); // Jump through hoops to get the hash-map keys as a String[], // because we can't just do a simple cast on the array. final Object[] ks = resources.keySet().toArray(); final String[] strArray = new String[ks.length]; System.arraycopy(ks, 0, strArray, 0, ks.length); return strArray; } /** * Return high level information about all non-specialized components currently * known on the server. This retrieves a map from the server, which contains * description and URI for each component. * * @return Map with information about each component. * @throws RestxClientException */ public Map<String, DescUriHolder> getAllComponentNamesPlus() throws RestxClientException { return getAllComponentNamesPlus(false); } /** * Return high level information about all components currently known on the * server. This retrieves a map from the server, which contains description and * URI for each component. * * @param specialized Flag indicating whether we want to see the specialized * components. * @return Map with information about each component. * @throws RestxClientException */ public Map<String, DescUriHolder> getAllComponentNamesPlus(boolean specialized) throws RestxClientException { if (specialized) { specializedComponents = getDescUriMap(specializedUri); return specializedComponents; } else { components = getDescUriMap(componentUri); return components; } } /** * Return the names of all non-specialized components currently known on the * server. * * @return Names of all components. * @throws RestxClientException */ public String[] getAllComponentNames() throws RestxClientException { return getAllComponentNames(false); } /** * Return the names of all components currently known on the server. * * @param specialized Flag indicating whether we want to see the specialized * components. * @return Names of all components. * @throws RestxClientException */ public String[] getAllComponentNames(boolean specialized) throws RestxClientException { // Refresh the component list. getAllComponentNamesPlus(specialized); // Jump through hoops to get the hash-map keys as a String[], // because we can't just do a simple cast on the array. final Object[] ks = components.keySet().toArray(); final String[] strArray = new String[ks.length]; System.arraycopy(ks, 0, strArray, 0, ks.length); return strArray; } /** * Return an initialized {@link RestxComponent} object for the specified * non-specialized component. * * @param name Name of the component. * @return Initialized {@link RestxComponent} object. * @throws RestxClientException */ public RestxComponent getComponent(String name) throws RestxClientException { return getComponent(name, false); } /** * Return an initialized {@link RestxComponent} object for the specified * component. * * @param name Name of the component. * @param specialized Flag indicating whether we want to get a specialized * components. * @return Initialized {@link RestxComponent} object. * @throws RestxClientException */ @SuppressWarnings("unchecked") public RestxComponent getComponent(String name, boolean specialized) throws RestxClientException { String uriPrefix; if (specialized) { uriPrefix = specializedUri; } else { uriPrefix = componentUri; } final HttpResult res = jsonSend(uriPrefix + "/" + name, null, null, 200, null); // The data of the HTTP result should be a dictionary with the component info return new RestxComponent(this, (Map<String, ?>) res.data); } /** * Return an initialized {@link RestxResource} object for the specified resource. * * @param name Name of the resource. * @return Initialized {@link RestxResource} object. * @throws RestxClientException */ @SuppressWarnings("unchecked") public RestxResource getResource(String name) throws RestxClientException { final HttpResult res = jsonSend(resourceUri + "/" + name, null, null, 200, null); // The data of the HTTP result should be a dictionary with the component info return new RestxResource(this, (Map<String, ?>) res.data); } }