/** * Copyright (C) 2009-2015 the original author or authors. * See the notice.md file distributed with this work for additional * information regarding copyright ownership. * * 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 org.fusesource.restygwt.client; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.fusesource.restygwt.rebind.AnnotationResolver; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; import com.google.gwt.http.client.RequestCallback; import com.google.gwt.http.client.RequestException; import com.google.gwt.http.client.Response; import com.google.gwt.json.client.JSONException; import com.google.gwt.json.client.JSONParser; import com.google.gwt.json.client.JSONValue; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.xml.client.Document; import com.google.gwt.xml.client.XMLParser; /** * * @author <a href="http://hiramchirino.com">Hiram Chirino</a> */ public class Method { /** * GWT hides the full spectrum of methods because safari has a bug: * http://bugs.webkit.org/show_bug.cgi?id=3812 * * We extend assume the server side will also check the * X-HTTP-Method-Override header. * * TODO: add an option to support using this approach to bypass restrictive * firewalls even if the browser does support the setting all the method * types. * * @author chirino */ static private class MethodRequestBuilder extends RequestBuilder { public MethodRequestBuilder(String method, String url) { super(method, url); //without null value being explicitly set gwt would generate "undefined" as a default value, //so if request does not have a body, Internet Explorer would send string "undefined" in the body of POST, PUT and DELETE requests, //which may cause the request to fall on server with "No operation matching request path" setRequestData(null); if(Defaults.isAddXHttpMethodOverrideHeader()){ setHeader("X-HTTP-Method-Override", method); } } } public RequestBuilder builder; final Set<Integer> expectedStatuses; { expectedStatuses = new HashSet<Integer>(); expectedStatuses.add(200); expectedStatuses.add(201); expectedStatuses.add(204); // This is needed for MSIE mangling with status 204 to become 1223 expectedStatuses.add(1223); } boolean anyStatus; Request request; Response response; Dispatcher dispatcher; /** * additional data which can be set per instance, e.g. from a {@link AnnotationResolver} */ private final Map<String, String> data = new HashMap<String, String>(); private Logger logger; protected Method() { } public Method(Resource resource, String method) { builder = new MethodRequestBuilder(method, resource.getUri()); } public Method user(String user) { builder.setUser(user); return this; } public Method password(String password) { builder.setPassword(password); return this; } public Method header(String header, String value) { builder.setHeader(header, value); return this; } public Method headers(Map<String, String> headers) { if (headers != null) { for (Entry<String, String> entry : headers.entrySet()) { builder.setHeader(entry.getKey(), entry.getValue()); } } return this; } private void doSetTimeout() { // Use default timeout only if it was not already set through the @Options(timeout =) annotation. // See https://github.com/resty-gwt/resty-gwt/issues/206 if (builder.getTimeoutMillis() == 0 && Defaults.getRequestTimeout() > -1) { builder.setTimeoutMillis(Defaults.getRequestTimeout()); } } public Method text(String data) { defaultContentType(Resource.CONTENT_TYPE_TEXT); builder.setRequestData(data); return this; } public Method json(JSONValue data) { defaultContentType(Resource.CONTENT_TYPE_JSON); builder.setRequestData(data.toString()); return this; } public Method xml(Document data) { defaultContentType(Resource.CONTENT_TYPE_XML); builder.setRequestData(data.toString()); return this; } public Method form(String encodedFormData) { defaultContentType(Resource.CONTENT_TYPE_FORM); builder.setRequestData(encodedFormData); return this; } public Method timeout(int timeout) { builder.setTimeoutMillis(timeout); return this; } /** * sets the expected response status code. If the response status code does not match * any of the values specified then the request is considered to have failed. Defaults to accepting * 200,201,204. If set to -1 then any status code is considered a success. */ public Method expect(int ... statuses) { if ( statuses.length==1 && statuses[0] < 0) { anyStatus = true; } else { anyStatus = false; this.expectedStatuses.clear(); for( int status : statuses ) { this.expectedStatuses.add(status); } } return this; } /** * Local file-system (file://) does not return any status codes. * Therefore - if we read from the file-system we accept all codes. * * This is for instance relevant when developing a PhoneGap application with * restyGwt. */ public boolean isExpected(int status) { String baseUrl = GWT.getHostPageBaseURL(); String requestUrl = builder.getUrl(); if (FileSystemHelper.isRequestGoingToFileSystem(baseUrl, requestUrl)) { return true; } else if (anyStatus) { return true; } else { return this.expectedStatuses.contains(status); } } public Object send(final RequestCallback callback) throws RequestException { doSetTimeout(); builder.setCallback(callback); // lazily load dispatcher from defaults, if one is not set yet. Dispatcher localDispatcher = dispatcher == null ? Defaults.getDispatcher() : dispatcher; return localDispatcher.send(this, builder); } private Logger getLogger() { if (GWT.isClient() && LogConfiguration.loggingIsEnabled() && this.logger == null) { this.logger = Logger.getLogger( Method.class.getName() ); } return this.logger; } public Object send(final TextCallback callback) { return send((MethodCallback<String>) callback); } public Object send(final MethodCallback<String> callback) { defaultAcceptType(Resource.CONTENT_TYPE_TEXT); try { return send(new AbstractRequestCallback<String>(this, callback) { @Override protected String parseResult() throws Exception { return response.getText(); } }); } catch (Throwable e) { if( getLogger() != null ){ getLogger().log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e); } callback.onFailure(this, e); return null; } } public Object send(final JsonCallback callback) { defaultAcceptType(Resource.CONTENT_TYPE_JSON); try { return send(new AbstractRequestCallback<JSONValue>(this, callback) { @Override protected JSONValue parseResult() throws Exception { try { return JSONParser.parseStrict(response.getText()); } catch (Throwable e) { throw new ResponseFormatException("Response was NOT a valid JSON document", e); } } }); } catch (Throwable e) { if( getLogger() != null ){ getLogger().log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e); } callback.onFailure(this, e); return null; } } public Object send(final XmlCallback callback) { defaultAcceptType(Resource.CONTENT_TYPE_XML); try { return send(new AbstractRequestCallback<Document>(this, callback) { @Override protected Document parseResult() throws Exception { try { return XMLParser.parse(response.getText()); } catch (Throwable e) { throw new ResponseFormatException("Response was NOT a valid XML document", e); } } }); } catch (Throwable e) { if( getLogger() != null ){ getLogger().log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e); } callback.onFailure(this, e); return null; } } public <T extends JavaScriptObject> Object send(final OverlayCallback<T> callback) { defaultAcceptType(Resource.CONTENT_TYPE_JSON); try { return send(new AbstractRequestCallback<T>(this, callback) { @SuppressWarnings("unchecked") @Override protected T parseResult() throws Exception { try { JSONValue val = JSONParser.parseStrict(response.getText()); if (val.isObject() != null) { return (T) val.isObject().getJavaScriptObject(); } else if (val.isArray() != null) { return (T) val.isArray().getJavaScriptObject(); } else { throw new ResponseFormatException("Response was NOT a JSON object"); } } catch (JSONException e) { throw new ResponseFormatException("Response was NOT a valid JSON document", e); } catch (IllegalArgumentException e) { throw new ResponseFormatException("Response was NOT a valid JSON document", e); } } }); } catch (Throwable e) { if( getLogger() != null ){ getLogger().log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e); } callback.onFailure(this, e); return null; } } public Request getRequest() { return request; } public Response getResponse() { return response; } protected void defaultContentType(String type) { if (builder.getHeader(Resource.HEADER_CONTENT_TYPE) == null) { header(Resource.HEADER_CONTENT_TYPE, type); } } protected void defaultAcceptType(String type) { if (builder.getHeader(Resource.HEADER_ACCEPT) == null) { header(Resource.HEADER_ACCEPT, type); } } public Dispatcher getDispatcher() { return dispatcher; } public void setDispatcher(Dispatcher dispatcher) { this.dispatcher = dispatcher; } /** * add some information onto the method which could be interesting when this method * comes back to the dispatcher. * * @param key * @param value */ public void addData(String key, String value) { data.put(key, value); } /** * get all data fields which was previously added * * @return */ public Map<String, String> getData() { return data; } }