/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ package fedora.server.messaging; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.UUID; import org.apache.abdera.Abdera; import org.apache.abdera.model.Category; import org.apache.abdera.model.Document; import org.apache.abdera.model.Entry; import org.apache.abdera.parser.Parser; import org.apache.log4j.Logger; import org.dom4j.DocumentException; import org.trippi.RDFFormat; import org.trippi.TrippiException; import fedora.server.storage.types.TupleArrayTripleIterator; import fedora.common.Constants; import fedora.server.errors.MessagingException; import fedora.server.storage.types.RelationshipTuple; import fedora.server.utilities.DateUtility; /** * Representation of an API-M method call as an Atom entry. * <ul> * <li>atom:title corresponds to the method name, e.g. ingest</li> * <li>Each atom:category corresponds to a method's argument: * <ul> * <li>The scheme indicates the argument name</li> * <li>The term indicates the argument value. However, null values are * indicated as "null", and non-null <code>xsd:base64Binary</code> values are * indicated as "[OMITTED]".</li> * <li>The label indicates the argument datatype</li> * </ul> * </li> * <li>atom:content corresponds to the textual representation of the method's * return value, noting the following: * <ul> * <li>Null values are represented as "null".</li> * <li><code>fedora-types:ArrayOfString</code> values are represented as a * comma-separated list, e.g. "value1, value2, value3".</li> * <li>Non-null <code>xsd:base64Binary</code> values are not returned, and * only indicated as "[OMITTED]".</li> * <li>Non-null <code>fedora-types:Datastream</code> values are not returned, * and only indicated as "[OMITTED]".</li> * <li><code>fedora-types:RelationshipTuple</code> values are represented in * Notation3 (N3).</li> * </ul> * </li> * <li>atom:uri element of atom:author corresponds to the baseURL of the Fedora * repository, e.g. http://localhost:8080/fedora.</li> * <li>atom:summary corresponds to the PID of the method, if applicable.</li> * </ul> * * @see <a href="http://atomenabled.org/developers/syndication/atom-format-spec.php">The Atom Syndication Format</a> * * @author Edwin Shin * @since 3.0 * @version $Id$ */ public class AtomAPIMMessage implements APIMMessage { /** Logger for this class. */ private static Logger LOG = Logger.getLogger(AtomAPIMMessage.class.getName()); private Abdera abdera = Abdera.getInstance(); private final static String TYPES_NS = Constants.TYPES.uri; private final static String TYPES_PREFIX = "fedora-types"; private final static String versionPredicate = Constants.VIEW.VERSION.uri; private final static String formatPredicate = "http://www.fedora.info/definitions/1/0/types/formatURI"; private static FedoraTypes fedoraTypes; private String fedoraBaseUrl; private String serverVersion; private String format; private String methodName; private String pid; private Date date; private String author; private Method method; private Object[] args; private Object returnVal; private Entry entry; public AtomAPIMMessage(FedoraMethod method, String fedoraBaseUrl, String serverVersion, String format) throws MessagingException { if (fedoraTypes == null) { try { fedoraTypes = new FedoraTypes(); } catch (FileNotFoundException e) { throw new MessagingException(e.getMessage(), e); } catch (DocumentException e) { throw new MessagingException(e.getMessage(), e); } } this.method = method.getMethod(); this.args = method.getParameters(); returnVal = method.getReturnValue(); this.fedoraBaseUrl = fedoraBaseUrl; this.serverVersion = serverVersion; this.format = format; methodName = method.getName(); pid = method.getPID() == null ? "" : method.getPID().toString(); date = method.getDate() == null ? new Date() : method.getDate(); if (method.getContext() != null) { author = method.getContext() .getSubjectValue(Constants.SUBJECT.LOGIN_ID.uri); } else { author = "unknown"; } entry = abdera.getFactory().newEntry(); entry.declareNS(Constants.XML_XSD.uri, "xsd"); entry.declareNS(TYPES_NS, TYPES_PREFIX); setEntryId(); setUpdated(); setAuthor(); setTitle(); addMethodParameters(); if (pid != null || !pid.equals("")) { entry.setSummary(pid); } setReturnValue(); if (serverVersion != null && !serverVersion.equals("")) entry.addCategory(versionPredicate, serverVersion, null); if (format != null && !format.equals("")) entry.addCategory(formatPredicate, format, null); } public AtomAPIMMessage(String messageText) { Parser parser = abdera.getParser(); Document<Entry> entryDoc = parser.parse(new StringReader(messageText)); entry = entryDoc.getRoot(); methodName = entry.getTitle(); date = entry.getUpdated(); author = entry.getAuthor().getName(); fedoraBaseUrl = entry.getAuthor().getUri().toString(); pid = entry.getSummary(); returnVal = entry.getContent(); serverVersion = getCategoryTerm(versionPredicate); format = getCategoryTerm(formatPredicate); } private void setEntryId() { entry.setId("urn:uuid:" + UUID.randomUUID().toString()); } /** * Set the entry's atom:author element using author from the Context * if it was available. Set the author:uri to fedoraBaseUrl. */ private void setAuthor() { entry.addAuthor(author, null, fedoraBaseUrl); } private void setTitle() { entry.setTitle(methodName); } private void setUpdated() { entry.setUpdated(date); } private void addMethodParameters() { Annotation[][] annotations = method.getParameterAnnotations(); for (int i = 0; i < annotations.length; i++) { String parameter = getParameterName(method, i); String datatype = fedoraTypes.getDatatype(methodName, parameter); if (datatype != null) { String scheme = TYPES_PREFIX + ":" + parameter; entry.addCategory(scheme, objectToString(args[i], datatype), datatype); } else { // parameters not defined in the WSDL are silently dropped (e.g. Context) if (LOG.isDebugEnabled()) { LOG.debug("Silently dropping parameter not defined in the WSDL: " + parameter); } } } } private void setReturnValue() { String m = methodName + "Response"; String parameter = fedoraTypes.getResponseParameter(m); String datatype = fedoraTypes.getDatatype(m, parameter); String term = objectToString(returnVal, datatype); entry.setContent(term); } /** * Get the String value of an object based on its class or * XML Schema Datatype. * * @param obj * @param xsdType * @return */ private String objectToString(Object obj, String xsdType) { if (obj == null) { return "null"; } String javaType = obj.getClass().getCanonicalName(); String term; if (javaType.equals("java.util.Date")) { term = DateUtility.convertDateToXSDString((Date) obj); } else if (xsdType.equals("fedora-types:ArrayOfString")) { term = array2string(obj); } else if (xsdType.equals("xsd:boolean")) { term = obj.toString(); } else if (xsdType.equals("xsd:nonNegativeInteger")) { term = obj.toString(); } else if (xsdType.equals("fedora-types:RelationshipTuple")) { RelationshipTuple[] tuples = (RelationshipTuple[]) obj; TupleArrayTripleIterator iter = new TupleArrayTripleIterator(new ArrayList<RelationshipTuple>(Arrays .asList(tuples))); ByteArrayOutputStream os = new ByteArrayOutputStream(); try { iter.toStream(os, RDFFormat.NOTATION_3, false); } catch (TrippiException e) { e.printStackTrace(); } term = new String(os.toByteArray()); } else if (javaType.equals("java.lang.String")) { term = (String) obj; term = term.replaceAll("\"", "'"); } else { term = "[OMITTED]"; } return term; } /** * Serialization of the API-M message as an Atom entry. {@inheritDoc} */ public String toString() { Writer sWriter = new StringWriter(); try { entry.writeTo("prettyxml", sWriter); } catch (IOException e) { LOG.error(e.getMessage()); } return sWriter.toString(); } /** * * {@inheritDoc} */ public String getBaseUrl() { return fedoraBaseUrl; } /** * * {@inheritDoc} */ public Date getDate() { return date; } /** * * {@inheritDoc} */ public String getMethodName() { return methodName; } /** * * {@inheritDoc} */ public String getPID() { return pid; } public String getAuthor() { return author; } /** * * {@inheritDoc} */ public String getFormat() { return format; } /** * * {@inheritDoc} */ public String getServerVersion() { return serverVersion; } private static <A extends Annotation> A getParameterAnnotation(Method m, int paramIndex, Class<A> annot) { for (Annotation a : m.getParameterAnnotations()[paramIndex]) { if (annot.isInstance(a)) return annot.cast(a); } return null; } /** * Get the name of a method parameter via its <code>PName</code> annotation. * * @param m * @param paramIndex the index of the parameter array. * @return the parameter name or an empty string if not available. */ private static String getParameterName(Method m, int paramIndex) { PName pName = getParameterAnnotation(m, paramIndex, PName.class); if (pName != null) { return pName.value(); } else { return ""; } } private static String array2string(Object array) { String nullstring = "null"; String delimiter = ", "; if (array == null) { return nullstring; } Object obj = null; int length = Array.getLength(array); int lastItem = length - 1; StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { obj = Array.get(array, i); if (obj != null) { sb.append(obj); } else { sb.append(nullstring); } if (i < lastItem) { sb.append(delimiter); } } return sb.toString(); } /** * Get the first atom:category term that matches the provided scheme. * * @param scheme * @return the term or null if no match. */ private String getCategoryTerm(String scheme) { List<Category> categories = entry.getCategories(scheme); if (categories.isEmpty()) { return null; } else { return categories.get(0).getTerm(); } } }