1 | /** |
2 | * Licensed to jclouds, Inc. (jclouds) under one or more |
3 | * contributor license agreements. See the NOTICE file |
4 | * distributed with this work for additional information |
5 | * regarding copyright ownership. jclouds licenses this file |
6 | * to you under the Apache License, Version 2.0 (the |
7 | * "License"); you may not use this file except in compliance |
8 | * with the License. You may obtain a copy of the License at |
9 | * |
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | * |
12 | * Unless required by applicable law or agreed to in writing, |
13 | * software distributed under the License is distributed on an |
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
15 | * KIND, either express or implied. See the License for the |
16 | * specific language governing permissions and limitations |
17 | * under the License. |
18 | */ |
19 | package org.jclouds.http.functions; |
20 | |
21 | import static com.google.common.base.Preconditions.checkArgument; |
22 | import static com.google.common.base.Preconditions.checkNotNull; |
23 | import static com.google.common.io.Closeables.closeQuietly; |
24 | |
25 | import java.io.IOException; |
26 | import java.io.InputStream; |
27 | import java.io.StringReader; |
28 | |
29 | import javax.inject.Inject; |
30 | |
31 | import org.jclouds.http.HttpRequest; |
32 | import org.jclouds.http.HttpResponse; |
33 | import org.jclouds.rest.InvocationContext; |
34 | import org.jclouds.rest.internal.GeneratedHttpRequest; |
35 | import org.jclouds.util.Strings2; |
36 | import org.xml.sax.InputSource; |
37 | import org.xml.sax.SAXException; |
38 | import org.xml.sax.SAXParseException; |
39 | import org.xml.sax.XMLReader; |
40 | import org.xml.sax.helpers.DefaultHandler; |
41 | |
42 | import com.google.common.base.Function; |
43 | import com.google.common.base.Throwables; |
44 | |
45 | /** |
46 | * This object will parse the body of an HttpResponse and return the result of type <T> back to the |
47 | * caller. |
48 | * |
49 | * @author Adrian Cole |
50 | */ |
51 | public class ParseSax<T> implements Function<HttpResponse, T>, InvocationContext<ParseSax<T>> { |
52 | |
53 | private final XMLReader parser; |
54 | private final HandlerWithResult<T> handler; |
55 | private HttpRequest request; |
56 | |
57 | public static interface Factory { |
58 | <T> ParseSax<T> create(HandlerWithResult<T> handler); |
59 | } |
60 | |
61 | @Inject |
62 | public ParseSax(XMLReader parser, HandlerWithResult<T> handler) { |
63 | this.parser = checkNotNull(parser, "parser"); |
64 | this.handler = checkNotNull(handler, "handler"); |
65 | } |
66 | |
67 | public T apply(HttpResponse from) { |
68 | try { |
69 | checkNotNull(from, "http response"); |
70 | checkNotNull(from.getPayload(), "payload in " + from); |
71 | } catch (NullPointerException e) { |
72 | return addDetailsAndPropagate(from, e); |
73 | } |
74 | if (from.getStatusCode() >= 300) |
75 | return convertStreamToStringAndParse(from); |
76 | InputStream is = from.getPayload().getInput(); |
77 | try { |
78 | return parse(new InputSource(is)); |
79 | } catch (RuntimeException e) { |
80 | return addDetailsAndPropagate(from, e); |
81 | } finally { |
82 | closeQuietly(is); |
83 | } |
84 | } |
85 | |
86 | private T convertStreamToStringAndParse(HttpResponse from) { |
87 | try { |
88 | return parse(Strings2.toStringAndClose(from.getPayload().getInput())); |
89 | } catch (Exception e) { |
90 | return addDetailsAndPropagate(from, e); |
91 | } |
92 | } |
93 | |
94 | public T parse(String from) { |
95 | try { |
96 | checkNotNull(from, "xml string"); |
97 | checkArgument(from.indexOf('<') >= 0, String.format("not an xml document [%s] ", from)); |
98 | } catch (RuntimeException e) { |
99 | return addDetailsAndPropagate(null, e); |
100 | } |
101 | return parse(new InputSource(new StringReader(from))); |
102 | } |
103 | |
104 | public T parse(InputStream from) { |
105 | try { |
106 | return parse(new InputSource(from)); |
107 | } finally { |
108 | closeQuietly(from); |
109 | } |
110 | } |
111 | |
112 | public T parse(InputSource from) { |
113 | try { |
114 | return doParse(from); |
115 | } catch (Exception e) { |
116 | return addDetailsAndPropagate(null, e); |
117 | } |
118 | } |
119 | |
120 | protected T doParse(InputSource from) throws IOException, SAXException { |
121 | checkNotNull(from, "xml inputsource"); |
122 | from.setEncoding("UTF-8"); |
123 | parser.setContentHandler(getHandler()); |
124 | // This method should accept documents with a BOM (Byte-order mark) |
125 | parser.parse(from); |
126 | return getHandler().getResult(); |
127 | } |
128 | |
129 | public T addDetailsAndPropagate(HttpResponse response, Exception e) { |
130 | StringBuilder message = new StringBuilder(); |
131 | if (request != null) { |
132 | message.append("request: ").append(request.getRequestLine()); |
133 | } |
134 | if (response != null) { |
135 | if (message.length() != 0) |
136 | message.append("; "); |
137 | message.append("response: ").append(response.getStatusLine()); |
138 | } |
139 | if (e instanceof SAXParseException) { |
140 | SAXParseException parseException = (SAXParseException) e; |
141 | String systemId = parseException.getSystemId(); |
142 | if (systemId == null) { |
143 | systemId = ""; |
144 | } |
145 | if (message.length() != 0) |
146 | message.append("; "); |
147 | message.append(String.format("error at %d:%d in document %s", parseException.getColumnNumber(), |
148 | parseException.getLineNumber(), systemId)); |
149 | } |
150 | if (message.length() != 0) { |
151 | message.append("; cause: ").append(e.toString()); |
152 | throw new RuntimeException(message.toString(), e); |
153 | } else { |
154 | Throwables.propagate(e); |
155 | return null; |
156 | } |
157 | |
158 | } |
159 | |
160 | public HandlerWithResult<T> getHandler() { |
161 | return handler; |
162 | } |
163 | |
164 | /** |
165 | * Handler that produces a useable domain object accessible after parsing completes. |
166 | * |
167 | * @author Adrian Cole |
168 | */ |
169 | public abstract static class HandlerWithResult<T> extends DefaultHandler implements |
170 | InvocationContext<HandlerWithResult<T>> { |
171 | private HttpRequest request; |
172 | |
173 | protected HttpRequest getRequest() { |
174 | return request; |
175 | } |
176 | |
177 | public abstract T getResult(); |
178 | |
179 | @Override |
180 | public HandlerWithResult<T> setContext(HttpRequest request) { |
181 | this.request = request; |
182 | return this; |
183 | } |
184 | } |
185 | |
186 | public abstract static class HandlerForGeneratedRequestWithResult<T> extends HandlerWithResult<T> { |
187 | @Override |
188 | protected GeneratedHttpRequest<?> getRequest() { |
189 | return (GeneratedHttpRequest<?>) super.getRequest(); |
190 | } |
191 | |
192 | @Override |
193 | public HandlerForGeneratedRequestWithResult<T> setContext(HttpRequest request) { |
194 | checkArgument(request instanceof GeneratedHttpRequest<?>, "note this handler requires a GeneratedHttpRequest"); |
195 | super.setContext(request); |
196 | return this; |
197 | } |
198 | } |
199 | |
200 | @Override |
201 | public ParseSax<T> setContext(HttpRequest request) { |
202 | handler.setContext(request); |
203 | this.request = request; |
204 | return this; |
205 | } |
206 | } |