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    }