001 /**
002 * Copyright (C) 2009-2011 the original author or authors.
003 * See the notice.md file distributed with this work for additional
004 * information regarding copyright ownership.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019 package org.fusesource.restygwt.client;
020
021 import java.util.HashMap;
022 import java.util.HashSet;
023 import java.util.Map;
024 import java.util.Map.Entry;
025 import java.util.Set;
026 import java.util.logging.Logger;
027
028 import org.fusesource.restygwt.rebind.AnnotationResolver;
029
030 import com.google.gwt.core.client.GWT;
031 import com.google.gwt.core.client.JavaScriptObject;
032 import com.google.gwt.http.client.Request;
033 import com.google.gwt.http.client.RequestBuilder;
034 import com.google.gwt.http.client.RequestCallback;
035 import com.google.gwt.http.client.RequestException;
036 import com.google.gwt.http.client.Response;
037 import com.google.gwt.json.client.JSONException;
038 import com.google.gwt.json.client.JSONParser;
039 import com.google.gwt.json.client.JSONValue;
040 import com.google.gwt.xml.client.Document;
041 import com.google.gwt.xml.client.XMLParser;
042 /**
043 *
044 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
045 */
046 public class Method {
047
048 /**
049 * GWT hides the full spectrum of methods because safari has a bug:
050 * http://bugs.webkit.org/show_bug.cgi?id=3812
051 *
052 * We extend assume the server side will also check the
053 * X-HTTP-Method-Override header.
054 *
055 * TODO: add an option to support using this approach to bypass restrictive
056 * firewalls even if the browser does support the setting all the method
057 * types.
058 *
059 * @author chirino
060 */
061 static private class MethodRequestBuilder extends RequestBuilder {
062 public MethodRequestBuilder(String method, String url) {
063
064 super(method, url);
065
066 setHeader("X-HTTP-Method-Override", method);
067 }
068 }
069
070 public RequestBuilder builder;
071
072 final Set<Integer> expectedStatuses;
073 {
074 expectedStatuses = new HashSet<Integer>();
075 expectedStatuses.add(200);
076 expectedStatuses.add(201);
077 expectedStatuses.add(204);
078 };
079 boolean anyStatus;
080
081 Request request;
082 Response response;
083 Dispatcher dispatcher = Defaults.getDispatcher();
084
085 /**
086 * additional data which can be set per instance, e.g. from a {@link AnnotationResolver}
087 */
088 private final Map<String, String> data = new HashMap<String, String>();
089
090 protected Method() {
091 }
092
093 public Method(Resource resource, String method) {
094 builder = new MethodRequestBuilder(method, resource.getUri());
095 }
096
097 public Method user(String user) {
098 builder.setUser(user);
099 return this;
100 }
101
102 public Method password(String password) {
103 builder.setPassword(password);
104 return this;
105 }
106
107 public Method header(String header, String value) {
108 builder.setHeader(header, value);
109 return this;
110 }
111
112 public Method headers(Map<String, String> headers) {
113 if (headers != null) {
114 for (Entry<String, String> entry : headers.entrySet()) {
115 builder.setHeader(entry.getKey(), entry.getValue());
116 }
117 }
118 return this;
119 }
120
121 private void doSetTimeout() {
122 if (Defaults.getRequestTimeout() > -1) {
123 builder.setTimeoutMillis(Defaults.getRequestTimeout());
124 }
125 }
126
127 public Method text(String data) {
128 defaultContentType(Resource.CONTENT_TYPE_TEXT);
129 builder.setRequestData(data);
130 return this;
131 }
132
133 public Method json(JSONValue data) {
134 defaultContentType(Resource.CONTENT_TYPE_JSON);
135 builder.setRequestData(data.toString());
136
137
138 return this;
139 }
140
141 public Method xml(Document data) {
142 defaultContentType(Resource.CONTENT_TYPE_XML);
143 builder.setRequestData(data.toString());
144 return this;
145 }
146
147 public Method timeout(int timeout) {
148 builder.setTimeoutMillis(timeout);
149 return this;
150 }
151
152 /**
153 * sets the expected response status code. If the response status code does not match
154 * any of the values specified then the request is considered to have failed. Defaults to accepting
155 * 200,201,204. If set to -1 then any status code is considered a success.
156 */
157 public Method expect(int ... statuses) {
158 if ( statuses.length==1 && statuses[0] < 0) {
159 anyStatus = true;
160 } else {
161 anyStatus = false;
162 this.expectedStatuses.clear();
163 for( int status : statuses ) {
164 this.expectedStatuses.add(status);
165 }
166 }
167 return this;
168 }
169
170 /**
171 * Local file-system (file://) does not return any status codes.
172 * Therefore - if we read from the file-system we accept all codes.
173 *
174 * This is for instance relevant when developing a PhoneGap application with
175 * restyGwt.
176 */
177 public boolean isExpected(int status) {
178
179 String baseUrl = GWT.getHostPageBaseURL();
180 String requestUrl = builder.getUrl();
181
182 if (FileSystemHelper.isRequestGoingToFileSystem(baseUrl, requestUrl)) {
183 return true;
184 } else if (anyStatus) {
185 return true;
186 } else {
187 return this.expectedStatuses.contains(status);
188 }
189 }
190
191 public void send(final RequestCallback callback) throws RequestException {
192 doSetTimeout();
193 builder.setCallback(callback);
194 dispatcher.send(this, builder);
195 }
196
197 public void send(final TextCallback callback) {
198 defaultAcceptType(Resource.CONTENT_TYPE_TEXT);
199 try {
200 send(new AbstractRequestCallback<String>(this, callback) {
201 protected String parseResult() throws Exception {
202 return response.getText();
203 }
204 });
205 } catch (Throwable e) {
206 GWT.log("Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
207 callback.onFailure(this, e);
208 }
209 }
210
211 public void send(final JsonCallback callback) {
212 defaultAcceptType(Resource.CONTENT_TYPE_JSON);
213
214 try {
215 send(new AbstractRequestCallback<JSONValue>(this, callback) {
216 protected JSONValue parseResult() throws Exception {
217 try {
218 return JSONParser.parseStrict(response.getText());
219 } catch (Throwable e) {
220 throw new ResponseFormatException("Response was NOT a valid JSON document", e);
221 }
222 }
223 });
224 } catch (Throwable e) {
225 GWT.log("Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
226 callback.onFailure(this, e);
227 }
228 }
229
230 public void send(final XmlCallback callback) {
231 defaultAcceptType(Resource.CONTENT_TYPE_XML);
232 try {
233 send(new AbstractRequestCallback<Document>(this, callback) {
234 protected Document parseResult() throws Exception {
235 try {
236 return XMLParser.parse(response.getText());
237 } catch (Throwable e) {
238 throw new ResponseFormatException("Response was NOT a valid XML document", e);
239 }
240 }
241 });
242 } catch (Throwable e) {
243 GWT.log("Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
244 callback.onFailure(this, e);
245 }
246 }
247
248 public <T extends JavaScriptObject> void send(final OverlayCallback<T> callback) {
249
250
251 defaultAcceptType(Resource.CONTENT_TYPE_JSON);
252 try {
253 send(new AbstractRequestCallback<T>(this, callback) {
254 protected T parseResult() throws Exception {
255 try {
256 JSONValue val = JSONParser.parseStrict(response.getText());
257 if (val.isObject() != null) {
258 return (T) val.isObject().getJavaScriptObject();
259 } else if (val.isArray() != null) {
260 return (T) val.isArray().getJavaScriptObject();
261 } else {
262 throw new ResponseFormatException("Response was NOT a JSON object");
263 }
264 } catch (JSONException e) {
265 throw new ResponseFormatException("Response was NOT a valid JSON document", e);
266 } catch (IllegalArgumentException e) {
267 throw new ResponseFormatException("Response was NOT a valid JSON document", e);
268 }
269 }
270 });
271 } catch (Throwable e) {
272 GWT.log("Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
273 callback.onFailure(this, e);
274 }
275 }
276
277 public Request getRequest() {
278 return request;
279 }
280
281 public Response getResponse() {
282 return response;
283 }
284
285 protected void defaultContentType(String type) {
286 if (builder.getHeader(Resource.HEADER_CONTENT_TYPE) == null) {
287 header(Resource.HEADER_CONTENT_TYPE, type);
288 }
289 }
290
291 protected void defaultAcceptType(String type) {
292 if (builder.getHeader(Resource.HEADER_ACCEPT) == null) {
293 header(Resource.HEADER_ACCEPT, type);
294 }
295 }
296
297 public Dispatcher getDispatcher() {
298 return dispatcher;
299 }
300
301 public void setDispatcher(Dispatcher dispatcher) {
302 this.dispatcher = dispatcher;
303 }
304
305 /**
306 * add some information onto the method which could be interesting when this method
307 * comes back to the dispatcher.
308 *
309 * @param key
310 * @param value
311 */
312 public void addData(String key, String value) {
313 data.put(key, value);
314 }
315
316 /**
317 * get all data fields which was previously added
318 *
319 * @return
320 */
321 public Map<String, String> getData() {
322 return data;
323 }
324 }