/* Copyright (c) 2008 Google Inc.
*
* 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 com.google.gdata.util;
import com.google.gdata.util.common.base.CharEscapers;
import com.google.gdata.client.authn.oauthproxy.OAuthProxyProtocol;
import com.google.gdata.client.authn.oauthproxy.OAuthProxyResponse;
import com.google.gdata.util.ErrorContent.LocationType;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The ServiceException class is the base exception class used to
* indicate an error while processing a GDataRequest.
*
*
*
*
*/
public class ServiceException extends Exception {
// The instance variables of this class are package-private.
// This is done so that the class ServiceExceptionInitializer
// (which is in essence an extension of the HttpURLConnection
// constructor) can set them without violating the taboo about
// calling instance methods such as setters on a partially
// constructed object.
// Instance variables for HTTP
/** HTTP error code. */
int httpErrorCodeOverride = -1;
/**
* Optional HTTP headers to be output when HTTP response is generated.
* It is a mapping from header name to list of header values.
*/
Map<String, List<String>> httpHeaders;
/**
* Optional HTTP content type for a response.
*/
ContentType responseContentType;
/**
* Retrieve HTTP response body or error message.
* The content type must be set correctly.
*/
String responseBody;
// Structured error element.
ErrorElement errorElement = new ErrorElement();
/**
* Set to the exception we are filling during parsing.
*/
List<ServiceException> siblings =
new ArrayList<ServiceException>(1);
{
siblings.add(this);
}
// Server-side constructors for reporting exceptions to client
public ServiceException(String message) {
super(nullsafe(message));
httpHeaders = new HashMap<String, List<String>>();
}
public ServiceException(String message, Throwable cause) {
this(message);
initCause(cause);
}
public ServiceException(Throwable cause) {
this(cause.getMessage());
initCause(cause);
}
/**
* Initializes the ServiceException using the error response data from an
* HTTP connection. This constructor is used in the client library
* to approximately reconstitute the constructor as it appeared
* in the server.
* <p>This constructor uses a ServiceExceptionInitializer to do
* the work of parsing the connection and calling our setters to
* initialize our fields. The initializer object may also create
* sibling ServiceExceptions.
* @param httpConn is the http connection from which the error message
* (structured or simple) is read
* @throws IOException if network error receiving the error response
*/
public ServiceException(HttpURLConnection httpConn) throws IOException {
super(nullsafe(httpConn.getResponseMessage()));
ServiceExceptionInitializer initializer =
new ServiceExceptionInitializer(this);
try {
initializer.parse(httpConn);
} catch (ParseException pe) {
// Clean up after failed parse
errorElement = new ErrorElement();
siblings.clear();
siblings.add(this);
responseContentType = ContentType.TEXT_PLAIN;
}
}
/**
* Initializes the ServiceException using an {@link ErrorContent} object that
* encapsulates most of the information about the error. Most ErrorContent
* instances are declared in a subclass of {@link ErrorDomain} containing all
* the errors for a GData domain (service or portion of service).
*/
public ServiceException(ErrorContent errorCode) {
this(errorCode.getInternalReason());
this.errorElement = new ErrorElement(errorCode);
}
/**
* Initializes the ServiceException using an {@link ErrorContent} object that
* encapsulates most of the information about the error, and an embedded
* exception. Most ErrorContent instances are declared in a subclass of
* {@link ErrorDomain} containing all the errors for this GData domain
* (service or portion of service).
*/
public ServiceException(ErrorContent errorCode, Throwable cause) {
this(errorCode);
initCause(cause);
}
/**
* {@inheritDoc}
*
* <p>If {@code cause} is a {@link ServiceException}, it is added to
* the list of siblings so that the original exception appears when
* using XML error output.
*/
@Override
public Throwable initCause(Throwable cause) {
super.initCause(cause);
if (cause instanceof ServiceException) {
addSibling((ServiceException) cause);
}
return this;
}
// Private method used in constructors
private static String nullsafe(String src) {
return (src != null) ? src : "Exception message unavailable";
}
// Getters and setters
public int getHttpErrorCodeOverride() { return httpErrorCodeOverride; }
public void setHttpErrorCodeOverride(int v) { httpErrorCodeOverride = v; }
public ContentType getResponseContentType() {
return responseContentType;
}
public void setResponseContentType(ContentType v) {
if (v == null) {
throw new NullPointerException("Null content type");
}
responseContentType = v;
}
public String getResponseBody() {
return responseBody;
}
public void setResponseBody(String body) {
if (body == null) {
throw new NullPointerException("Null response body");
}
responseBody = body;
// Create and invoke a ServiceExceptionInitializer to
// do the dirty work of reading the stream, possibly
// parsing the result if it is a GData structured error,
// and set the instance variables accordingly.
// In C++ terms, SEI is a friend class.
ServiceExceptionInitializer initializer =
new ServiceExceptionInitializer(this);
try {
initializer.parse(responseContentType, responseBody);
} catch (ParseException pe) {
throw new RuntimeException(pe.getMessage(), pe);
}
}
/**
* Set HTTP response type and body simultaneously.
* They are inherently coupled together: a body without a content type
* is meaningless; a content type without a body is useless.
*/
public void setResponse(ContentType contentType, String body) {
if (contentType == null) {
throw new NullPointerException("Null content type");
}
if (body == null) {
throw new NullPointerException("Null response body");
}
responseContentType = contentType;
setResponseBody(body);
}
/** Generate error message in XML format. */
/**
* Converts the exception into a well-formated XML error
* message suitable for external uses.
*/
public String toXmlErrorMessage() {
return toXmlErrorMessage(false);
}
/**
* Converts the exception into a well-formated XML error
* message suitable for external uses.
*
* @param includeDebugInfo if {@code true}, include debug information.
* Such error message should only be returned to internal clients.
*/
public String toXmlErrorMessage(boolean includeDebugInfo) {
StringBuilder sb = new StringBuilder();
sb.append("<errors xmlns='http://schemas.google.com/g/2005'>\n");
for (ServiceException sibling : siblings) {
addXmlError(sibling, sb, includeDebugInfo);
}
sb.append("</errors>\n");
return sb.toString();
}
private String escape(String src) {
return CharEscapers.xmlEscaper().escape(src);
}
private void addXmlError(ServiceException se, StringBuilder sb,
boolean includeDebugInfo) {
// Simplistic StringBuffer implementation because the XML is trivial.
sb.append("<error>\n");
String domainName = se.getDomainName();
sb.append("<domain>");
sb.append(escape(domainName));
sb.append("</domain>\n");
String codeName = se.getCodeName();
sb.append("<code>");
sb.append(escape(codeName));
sb.append("</code>\n");
String location = se.getLocation();
LocationType locationType = se.getLocationTypeWithDefault();
if (location != null) {
sb.append("<location type='");
sb.append(escape(locationType.toString()));
sb.append("'>");
sb.append(escape(location));
sb.append("</location>\n");
}
String internalReason = se.getInternalReason();
if (internalReason != null) {
sb.append("<internalReason>");
sb.append(escape(internalReason));
sb.append("</internalReason>\n");
}
String extendedHelp = se.getExtendedHelp();
if (extendedHelp != null) {
sb.append("<extendedHelp>");
sb.append(escape(extendedHelp));
sb.append("</extendedHelp>\n");
}
String sendReport = se.getSendReport();
if (sendReport != null) {
sb.append("<sendReport>");
sb.append(escape(sendReport));
sb.append("</sendReport>\n");
}
if (includeDebugInfo) {
String debugInfo = se.getDebugInfo();
if (debugInfo != null) {
sb.append("<debugInfo>");
sb.append(escape(debugInfo));
sb.append("</debugInfo>\n");
}
}
sb.append("</error>\n");
}
/**
* Return the internal HTTP headers in modifiable form.
*/
public Map<String, List<String>> getHttpHeaders() { return httpHeaders; }
/**
* Return the value for requested internal HTTP header or {@code null} if
* the header is not present.
*
* @param header requested header name
* @return list of string values for the specified header or {@code null} if
* requested header is not found.
*/
public List<String> getHttpHeader(String header) {
if (header == null) {
return httpHeaders.get(header);
}
for (String key : httpHeaders.keySet()) {
if (key != null && key.toLowerCase().equals(header.toLowerCase())) {
return httpHeaders.get(key);
}
}
return null;
}
// Override the default Throwable toString() implementation to add
// the response body (either an explicitly set one or the default XML
// error message) to the resulting output. This is useful because the
// Throwable toString() output is included in exception stack traces,
// so this means the full response will be visible in traces.
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString());
if (responseBody != null) {
sb.append('\n');
sb.append(responseBody);
}
// purposes, but don't do this until all GData services are converted.
return sb.toString();
}
// Error model getters and setters
/**
* Return error domain.
*
* <p>Defaults to "GData", indicating an error that has
* not yet been upgraded to the new architecture.
*/
public String getDomainName() {
String domainName = errorElement.getDomainName();
return (domainName != null) ? domainName : "GData";
}
/**
* Set error domain.
*
* @throws NullPointerException if {@code domain} is {@code null}.
*/
public void setDomain(String domain) {
errorElement.setDomain(domain);
}
/**
* Return error code.
*
* <p>Defaults to the class name of {@code this}.
*/
public String getCodeName() {
String codeName = errorElement.getCodeName();
return (codeName != null) ? codeName : getClass().getSimpleName();
}
/**
* Set error code.
*
* @throws NullPointerException if {@code code} is {@code null}.
*/
public void setCode(String code) {
errorElement.setCode(code);
}
/**
* Return error location.
*/
public String getLocation() {
return errorElement.getLocation();
}
/**
* Return error location type.
*/
public LocationType getLocationType() {
return errorElement.getLocationType();
}
/**
* Returns error location type, defaulting to {@link LocationType#OTHER}.
*/
private LocationType getLocationTypeWithDefault() {
LocationType type = getLocationType();
return type != null ? type : LocationType.OTHER;
}
/**
* Set XPath-based error location.
* This must be a valid XPath expression sibling to the atom:entry
* element (or the atom:feed element if we are not in an entry).
*
* @throws NullPointerException if {@code location} is {@code null}.
*/
public void setXpathLocation(String location) {
errorElement.setXpathLocation(location);
}
/**
* Set header name for an error in a header.
*
* @throws NullPointerException if {@code location} is {@code null}.
*/
public void setHeaderLocation(String location) {
errorElement.setHeaderLocation(location);
}
/**
* Set generic error location.
*
* @throws NullPointerException if {@code location} is {@code null}.
*/
public void setLocation(String location) {
errorElement.setLocation(location);
}
/**
* Return error internal reason.
*
* <p>Defaults to the message set at construction time.
*/
public String getInternalReason() {
String internalReason = errorElement.getInternalReason();
return (internalReason != null) ? internalReason : super.getMessage();
}
/**
* Return message: same as getInternalReason.
*/
@Override
public String getMessage() {
return getInternalReason();
}
/**
* Sets the internal reason of the error.
*
* @throws NullPointerException if {@code internalReason} is {@code null}.
*/
public void setInternalReason(String internalReason) {
errorElement.setInternalReason(internalReason);
}
/**
* Return URI for extended help
*/
public String getExtendedHelp() {
return errorElement.getExtendedHelp();
}
/**
* Set URI for extended help.
*
* @throws NullPointerException if {@code extendedHelp} is {@code null}.
*/
public void setExtendedHelp(String extendedHelp) {
errorElement.setExtendedHelp(extendedHelp);
}
/**
* Return URI to send report to.
*/
public String getSendReport() {
return errorElement.getSendReport();
}
/**
* Set URI to send report to.
*
* @throws NullPointerException if {@code sendReport} is {@code null}.
*/
public void setSendReport(String sendReport) {
errorElement.setSendReport(sendReport);
}
/**
* Return debugging information.
* Defaults to the stack trace.
*/
public String getDebugInfo() {
return errorElement.getDebugInfo();
}
/**
* Set debugging information.
*
* @throws NullPointerException if {@code debugInfo} is {@code null}.
*/
public void setDebugInfo(String debugInfo) {
errorElement.setDebugInfo(debugInfo);
}
// Logic for handling sibling exceptions.
// All the siblings, including this one. We keep ourselves
// in the sibling list so that all siblings can share a common
// list. There is no hierarchy among siblings, so throwing any
// of them will produce the same result.
/**
* Return an unmodifiable copy of the sibling list.
*/
public List<ServiceException> getSiblings() {
return Collections.unmodifiableList(
new ArrayList<ServiceException>(siblings));
}
/**
* Make {@tt this} and {@tt newbie} siblings, returning {@tt this}.
* All sibling exceptions are jointly converted to an
* error message when any of them are thrown.
*/
public ServiceException addSibling(ServiceException newbie) {
if (newbie == null) {
throw new NullPointerException("Null exception being added");
}
for (ServiceException newbieSibling : newbie.siblings) {
if (!siblings.contains(newbieSibling)) {
siblings.add(newbieSibling);
}
newbieSibling.siblings = siblings;
}
return this;
}
/**
* Return true if this ServiceException matches the specified
* {@link ErrorContent} in domain name and code name. Sibling exceptions are
* not checked.
*/
public boolean matches(ErrorContent code) {
return getDomainName().equals(code.getDomainName())
&& getCodeName().equals(code.getCodeName());
}
/**
* Return true if this ServiceException or any of its sibling exceptions
* matches the specified {@link ErrorContent} in domain name and code name.
* If you want to know <i>which</i> particular ServiceException matched, call
* {@link #getSiblings} and examine the individual ServiceExceptions with
* {@link #matches}.
*/
public boolean matchesAny(ErrorContent errorCode) {
for (ServiceException se : siblings) {
if (se.matches(errorCode)) {
return true;
}
}
return false;
}
/**
* Returns whether or not the request has OAuth Proxy-related headers.
*/
public boolean hasOAuthProxyResponse() {
return httpHeaders.containsKey(
OAuthProxyProtocol.Header.X_OAUTH_APPROVAL_URL)
|| httpHeaders.containsKey(OAuthProxyProtocol.Header.X_OAUTH_STATE)
|| httpHeaders.containsKey(OAuthProxyProtocol.Header.X_OAUTH_ERROR)
|| httpHeaders.containsKey(OAuthProxyProtocol.Header.X_OAUTH_ERROR);
}
/**
* Returns the headers related to the OAuth Proxy. If there are no OAuth
* Proxy related headers on the request, this method will return an
* {@link OAuthProxyResponse} object with null values for the headers. Use
* {@link #hasOAuthProxyResponse()} to determine whether the request has
* OAuth Proxy information.
*/
public OAuthProxyResponse getOAuthProxyResponse() {
return new OAuthProxyResponse(httpHeaders);
}
}