/* Copyright (c) 2006 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.api.gbase.client; import com.google.gdata.util.ServiceException; import com.google.gdata.util.ContentType; import com.google.gdata.util.XmlParser; import com.google.gdata.util.ParseException; import com.google.gdata.data.HtmlTextConstruct; import com.google.gdata.data.batch.BatchStatus; import org.xml.sax.Attributes; import java.util.Collection; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.List; import java.io.StringReader; import java.io.IOException; /** * Extracts and organizes error messages from a * {@link com.google.gdata.util.ServiceException} or from a * {@link com.google.gdata.data.batch.BatchStatus}. * * This object parses the body of a ServiceException * or the content of a BatchStatus * and gives programmatic access to the error messages embedded in the body * of the exception. */ public class ServiceErrors { /** Errors with type != data. */ private List<ServiceError> requestErrors = new ArrayList<ServiceError>(); /** Errors with type = data. */ private List<ServiceError> dataErrors = new ArrayList<ServiceError>(); private static final ContentType DEFAULT_CONTENT_TYPE = new ContentType("text/plain"); /** * Returns a convenient text representation, for debugging. */ @Override public String toString() { StringBuffer retval = new StringBuffer(); appendErrors(retval, requestErrors); appendErrors(retval, dataErrors); return retval.toString(); } private static void appendErrors(StringBuffer retval, List<ServiceError> list) { for (ServiceError error: list) { if (retval.length() > 0) { retval.append(", "); } retval.append(error.toString()); } } /** * Creates a ServiceErrors object corresponding * to the errors contained in a {@link ServiceException}. * * @param e */ public ServiceErrors(ServiceException e) { addErrors(e); } /** * Creates a ServiceErrors object corresponding * to the errors contained in {@link BatchStatus}. * * @param status */ public ServiceErrors(BatchStatus status) { addErrors(status); } /** * Empty constructor. */ public ServiceErrors() { } /** * Extracts errors from a {@link ServiceException}. * * @param e the ServiceException to be parsed */ public void addErrors(ServiceException e) { addErrors(e.getMessage(), e.getResponseContentType(), e.getResponseBody()); } /** * Extracts errors from a {@link BatchStatus}. * * @param status the BatchStatus to be parsed */ public void addErrors(BatchStatus status) { addErrors(status.getReason(), status.getContentType(), status.getContent()); } /** * Registers errors. * * @param reason short error message, if nothing else helps, this * will be used * @param contentType content type, if null the {@link #DEFAULT_CONTENT_TYPE} * will be used * @param body extended error message, or null */ private void addErrors(String reason, ContentType contentType, String body) { if (body == null) { addError(new ServiceError(reason)); return; } if (contentType == null) { contentType = DEFAULT_CONTENT_TYPE; } if (contentType.toString().startsWith("application/xml")) { try { XmlParser parser = new XmlParser(); parser.parse(new StringReader(body), new ErrorsElementHandler(), "", "errors"); } catch (IOException ioe) { // This should never happen, but if it does, treat it // like a Parse error. addInvalidXmlServiceError(reason, body); } catch (ParseException pe) { // Best effort: display the original error message // as text. We're already handling an error, let's // not hide it with another one. addInvalidXmlServiceError(reason, body); } } else if (contentType.toString().startsWith("text/html")) { // This usually happens when the feed URL actually // points to a web page. Just get the error message // from the HTML as more usable plain text. HtmlTextConstruct construct = new HtmlTextConstruct(body); addError(new ServiceError(construct.getPlainText())); } else { // Treat it as plain text addError(new ServiceError(body)); } } /** * If parsing the XML-formatted message failed, still * forward the message as a raw string, which is still * better than nothing... * * An invalid XML can be caused by either a bug on * the server or a wrong URL that would return an * XML file of a different nature. * * @param reason * @param body */ private void addInvalidXmlServiceError(String reason, String body) { addError(new ServiceError(reason + "(badly formatted xml error message: " + body)); } /** * Registers a new error. * * @param error */ public void addError(ServiceError error) { if (ServiceError.DATA_TYPE.equals(error.getType())) { dataErrors.add(error); } else { requestErrors.add(error); } } /** * Gets all errors. * * @return both request and data errors. May be empty but not null. */ public List<? extends ServiceError> getAllErrors() { List<ServiceError> retval = new ArrayList<ServiceError>(requestErrors.size() + dataErrors.size()); retval.addAll(requestErrors); retval.addAll(dataErrors); return retval; } /** * Gets non-data errors, which apply to the whole request. * * @return non-data errors. May be empty but not null. */ public List<? extends ServiceError> getRequestErrors() { return requestErrors; } /** * Gets data errors, which apply to the item content, often * to one field in particular. * * @return data errors. May be empty but not null. */ public Collection<? extends ServiceError> getDataErrors() { return dataErrors; } /** * Gets the set of all fields that have errors. * * @return field names. May be empty but not null. */ public Set<? extends String> getErrorFields() { Set<String> fields = new HashSet<String>(); for (ServiceError error: dataErrors) { String field = error.getField(); if (field != null) { fields.add(field); } } return fields; } /** * Gets all errors for one specific field. * * @param field field name, which usually comes from * {@link #getErrorFields()} * @return all errors applied to the field. May be empty but not null. */ public List<? extends ServiceError> getFieldErrors(String field) { List<ServiceError> retval = new ArrayList<ServiceError>(); for (ServiceError error: dataErrors) { if (equalsMaybeNull(field, error.getField())) { if (retval == null) { retval = new ArrayList<ServiceError>(); } retval.add(error); } } return retval; } private static boolean equalsMaybeNull(String a, String b) { if (a == null) { return b == null; } else { return a.equals(b); } } /** * Handles the root element, {@code <errors>}. */ private class ErrorsElementHandler extends XmlParser.ElementHandler { @Override public XmlParser.ElementHandler getChildHandler(String namespace, String localName, Attributes attrs) throws ParseException, IOException { if ("error".equals(localName)) { return new ErrorElementHandler(attrs); } return super.getChildHandler(namespace, localName, attrs); } } /** * Handles the element {@code <error>} and adds the corresponding * {@link ServiceError}. */ private class ErrorElementHandler extends XmlParser.ElementHandler { public ErrorElementHandler(Attributes attrs) { addError(new ServiceError(attrs.getValue("type"), attrs.getValue("field"), attrs.getValue("reason"))); } } }