001: /*
002: * Copyright 2004 Outerthought bvba and Schaubroeck nv
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.outerj.daisy.repository.clientimpl.infrastructure;
017:
018: import org.apache.commons.httpclient.*;
019: import org.apache.commons.httpclient.auth.AuthScope;
020: import org.apache.xmlbeans.XmlObject;
021: import org.apache.xmlbeans.XmlOptions;
022: import org.outerj.daisy.repository.RepositoryException;
023: import org.outerj.daisy.repository.AuthenticationFailedException;
024: import org.outerj.daisy.repository.RepositoryRuntimeException;
025: import org.outerj.daisy.xmlutil.LocalSAXParserFactory;
026: import org.outerx.daisy.x10.ErrorDocument;
027: import org.outerx.daisy.x10.CauseType;
028:
029: import java.util.Map;
030: import java.util.ArrayList;
031: import java.util.HashMap;
032: import java.util.List;
033: import java.util.concurrent.ConcurrentHashMap;
034: import java.lang.reflect.Method;
035: import java.lang.reflect.Constructor;
036: import java.lang.reflect.InvocationTargetException;
037:
038: public class DaisyHttpClient {
039: private static Map<Class, Method> xmlObjectParseMethodCache = new ConcurrentHashMap<Class, Method>(
040: 20, .75f, 2);
041: private static final boolean validateResponses = false;
042: private HttpClient sharedHttpClient;
043: private HttpState httpState;
044: private HostConfiguration sharedHostConfiguration;
045: private String login;
046:
047: public DaisyHttpClient(HttpClient sharedHttpClient,
048: HostConfiguration sharedHostConfiguration,
049: HttpState httpState, String login) {
050: this .sharedHttpClient = sharedHttpClient;
051: this .httpState = httpState;
052: this .sharedHostConfiguration = sharedHostConfiguration;
053: this .login = login;
054: }
055:
056: public static HttpState buildHttpState(String login,
057: String password, long[] activeRoleIds) {
058: HttpState httpState = new HttpState();
059: // @'s in the login should be escaped by doubling them
060: login = login.replaceAll("@", "@@");
061: if (activeRoleIds != null)
062: login += "@" + getActiveRoleString(activeRoleIds);
063: UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
064: login, password);
065: httpState.setCredentials(AuthScope.ANY, credentials);
066:
067: return httpState;
068: }
069:
070: private static String getActiveRoleString(long[] activeRoleIds) {
071: StringBuilder buffer = new StringBuilder(
072: activeRoleIds.length * 4);
073: for (int i = 0; i < activeRoleIds.length; i++) {
074: if (i > 0)
075: buffer.append(',');
076: buffer.append(activeRoleIds[i]);
077: }
078: return buffer.toString();
079: }
080:
081: /**
082: * Executes the given method, and handles the response to take care of exceptions
083: * or non-OK responses, and optionally parses the response body according to the specified
084: * XmlObject class. If this method returns without throwing an exception, one can
085: * assume that the execution of the HTTP method was successful.
086: *
087: * @param xmlObjectResponseClass an Apache XmlBeans generated class (having a Factory inner class).
088: * @return the XmlObject resulting from the parsing of the response body, or null if no XmlObject
089: * class was specified.
090: */
091: public XmlObject executeMethod(HttpMethod method,
092: Class xmlObjectResponseClass, boolean releaseConnection)
093: throws RepositoryException {
094: try {
095: int statusCode;
096: try {
097: statusCode = sharedHttpClient.executeMethod(
098: sharedHostConfiguration, method, httpState);
099: } catch (Exception e) {
100: throw new RepositoryException(
101: "Problems connecting to repository server.", e);
102: }
103:
104: if (statusCode == HttpStatus.SC_OK) {
105: if (xmlObjectResponseClass != null) {
106: Method parseMethod = getParseMethod(xmlObjectResponseClass);
107: XmlObject parseResult;
108: try {
109: parseResult = (XmlObject) parseMethod.invoke(
110: null, method.getResponseBodyAsStream());
111: if (validateResponses)
112: parseResult.validate();
113: } catch (Exception e) {
114: throw new RepositoryException(
115: "Error parsing reponse from repository server.",
116: e);
117: }
118:
119: return parseResult;
120: } else {
121: return null;
122: }
123: } else {
124: handleNotOkResponse(method);
125: // handleNotOkResponse always throws an exception, thus...
126: throw new RuntimeException(
127: "This statement should be unreacheable.");
128: }
129: } finally {
130: if (releaseConnection)
131: method.releaseConnection();
132: }
133: }
134:
135: private static Method getParseMethod(Class xmlObjectClass) {
136: Object parseMethod = xmlObjectParseMethodCache
137: .get(xmlObjectClass);
138: if (parseMethod != null) {
139: return (Method) parseMethod;
140: } else {
141: Class[] classes = xmlObjectClass.getClasses();
142: Class factoryClass = null;
143: for (Class clazz : classes) {
144: if (clazz.getName().equals(
145: xmlObjectClass.getName() + "$Factory")) {
146: factoryClass = clazz;
147: break;
148: }
149: }
150:
151: if (factoryClass == null) {
152: throw new RuntimeException(
153: "Missing Factory class in class "
154: + xmlObjectClass.getName());
155: }
156:
157: Method newParseMethod;
158: try {
159: newParseMethod = factoryClass.getMethod("parse",
160: java.io.InputStream.class);
161: } catch (NoSuchMethodException e) {
162: throw new RuntimeException(
163: "Missing parse method on XmlObject Factory class for "
164: + xmlObjectClass.getName(), e);
165: }
166:
167: xmlObjectParseMethodCache.put(xmlObjectClass,
168: newParseMethod);
169: return newParseMethod;
170: }
171: }
172:
173: public static String getContentType(HttpMethod method)
174: throws RepositoryException {
175: String contentType = null;
176: if (method.getResponseHeader("Content-Type") != null)
177: contentType = method.getResponseHeader("Content-Type")
178: .getElements()[0].getName();
179: return contentType;
180: }
181:
182: public void handleNotOkResponse(HttpMethod method)
183: throws RepositoryException {
184: if ("text/xml".equals(getContentType(method))) {
185: // an error occured server side
186: ErrorDocument.Error errorXml;
187: try {
188: XmlOptions xmlOptions = new XmlOptions()
189: .setLoadUseXMLReader(LocalSAXParserFactory
190: .newXmlReader());
191: ErrorDocument errorDocument = ErrorDocument.Factory
192: .parse(method.getResponseBodyAsStream(),
193: xmlOptions);
194: errorXml = errorDocument.getError();
195: } catch (Exception e) {
196: throw new RepositoryException(
197: "Error reading error response from repositoryserver",
198: e);
199: }
200: if (errorXml.getDescription() != null) {
201: throw new RepositoryException(
202: "Repository server answered with an error: "
203: + errorXml.getDescription());
204: } else {
205: CauseType causeXml = errorXml.getCause();
206: tryRestoreOriginalExceptionAndThrowIt(causeXml);
207: Exception cause = restoreException(causeXml);
208: throw new RepositoryException(
209: "Received exception from repository server.",
210: cause);
211: }
212: } else {
213: if (method.getStatusCode() == 401) {
214: throw new AuthenticationFailedException(this .login);
215: } else {
216: throw new RepositoryException(
217: "Unexpected response from repositoryserver: "
218: + method.getStatusCode()
219: + " : "
220: + HttpStatus.getStatusText(method
221: .getStatusCode()));
222: }
223: }
224: }
225:
226: private static Exception restoreException(CauseType causeXml) {
227: String message = causeXml.getException().getMessage();
228: String className = causeXml.getException().getType();
229:
230: List<MyStackTraceElement> stackTrace = new ArrayList<MyStackTraceElement>();
231: List<CauseType.StackTrace.StackTraceElement> stackTraceElements = causeXml
232: .getStackTrace().getStackTraceElementList();
233: for (CauseType.StackTrace.StackTraceElement stackTraceElement : stackTraceElements) {
234: stackTrace.add(new MyStackTraceElement(stackTraceElement
235: .getClassName(), stackTraceElement.getFileName(),
236: stackTraceElement.getLineNumber(),
237: stackTraceElement.getMethodName(),
238: stackTraceElement.getNativeMethod()));
239: }
240: MyStackTraceElement[] remoteStackTrace = stackTrace
241: .toArray(new MyStackTraceElement[0]);
242:
243: DaisyPropagatedException exception = new DaisyPropagatedException(
244: message, className, remoteStackTrace);
245:
246: CauseType nestedCauseXml = causeXml.getCause();
247: if (nestedCauseXml != null) {
248: Exception cause = restoreException(nestedCauseXml);
249: exception.initCause(cause);
250: }
251:
252: return exception;
253: }
254:
255: /**
256: * This method handles exceptions which can be restored, ie RepositoryException's
257: * whose getState method returned a Map and have a constructor that takes a Map
258: * as argument.
259: *
260: * <p>If the exception could be restored, this method will throw it immediatelly,
261: * otherwise it will simply return. Only call this method if there is actually
262: * ExceptionData, otherwise this will throw a NPE.
263: */
264: private static void tryRestoreOriginalExceptionAndThrowIt(
265: CauseType causeXml) throws RepositoryException {
266: RepositoryException restoredException = tryRestoreOriginalException(causeXml);
267: if (restoredException != null) {
268: throw restoredException;
269: }
270: }
271:
272: private static RepositoryException tryRestoreOriginalException(
273: CauseType causeXml) throws RepositoryException {
274: if (causeXml.getExceptionData() == null)
275: return null;
276:
277: String className = causeXml.getException().getType();
278: CauseType.ExceptionData exceptionData = causeXml
279: .getExceptionData();
280:
281: Map<String, String> state = new HashMap<String, String>();
282: for (CauseType.ExceptionData.Parameter parameter : exceptionData
283: .getParameterList()) {
284: state.put(parameter.getName(), parameter.getValue());
285: }
286:
287: Class clazz;
288: try {
289: clazz = DaisyHttpClient.class.getClassLoader().loadClass(
290: className);
291: } catch (ClassNotFoundException e) {
292: return null;
293: }
294:
295: if (!RepositoryException.class.isAssignableFrom(clazz)
296: && !RepositoryRuntimeException.class
297: .isAssignableFrom(clazz))
298: return null;
299:
300: Constructor constructor;
301: try {
302: constructor = clazz.getConstructor(Map.class);
303: } catch (NoSuchMethodException e) {
304: return null;
305: }
306:
307: RepositoryException restoredException;
308: try {
309: restoredException = (RepositoryException) constructor
310: .newInstance(state);
311: } catch (InstantiationException e) {
312: return null;
313: } catch (IllegalAccessException e) {
314: return null;
315: } catch (InvocationTargetException e) {
316: return null;
317: }
318:
319: if (causeXml.getCause() != null) {
320: Exception cause = null;
321: if (restoredException
322: .getClass()
323: .getName()
324: .equals(
325: "org.outerj.daisy.publisher.GlobalPublisherException")) {
326: cause = tryRestoreOriginalException(causeXml.getCause());
327: }
328: if (cause == null) {
329: cause = restoreException(causeXml.getCause());
330: }
331: restoredException.initCause(cause);
332: }
333:
334: return restoredException;
335: }
336: }
|