/* 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 org.fcrepo.server.rest; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.io.Writer; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import org.fcrepo.common.Constants; import org.fcrepo.server.Context; import org.fcrepo.server.Server; import org.fcrepo.server.access.ObjectProfile; import org.fcrepo.server.rest.RestUtil.RequestContent; import org.fcrepo.server.rest.param.DateTimeParam; import org.fcrepo.server.search.Condition; import org.fcrepo.server.search.FieldSearchQuery; import org.fcrepo.server.search.FieldSearchResult; import org.fcrepo.server.storage.types.Validation; import org.fcrepo.server.utilities.StreamUtility; import org.fcrepo.utilities.DateUtility; import org.fcrepo.utilities.ReadableByteArrayOutputStream; import org.fcrepo.utilities.ReadableCharArrayWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; /** * Implement /objects/pid/* REST API * * @author cuong.tran@yourmediashelf.com * @version $Id$ */ @Path("/") @Component public class FedoraObjectsResource extends BaseRestResource { private final String FOXML1_1 = "info:fedora/fedora-system:FOXML-1.1"; private final String ATOMZIP1_1 = "info:fedora/fedora-system:ATOMZip-1.1"; static final String[] SEARCHABLE_FIELDS = { "pid", "label", "state", "ownerId", "cDate", "mDate", "dcmDate", "title", "creator", "subject", "description", "publisher", "contributor", "date", "type", "format", "identifier", "source", "language", "relation", "coverage", "rights" }; private static final Logger logger = LoggerFactory.getLogger(FedoraObjectsResource.class); public FedoraObjectsResource(Server server) { super(server); } @GET @Path("/") @Produces( { HTML, XML }) public Response searchObjects( @QueryParam(RestParam.TERMS) String terms, @QueryParam(RestParam.QUERY) String query, @QueryParam(RestParam.MAX_RESULTS) @DefaultValue("25") int maxResults, @QueryParam(RestParam.SESSION_TOKEN) String sessionToken, @QueryParam(RestParam.RESULT_FORMAT) @DefaultValue(HTML) String format, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); String[] wantedFields = getWantedFields(m_servletRequest); MediaType mime = RestHelper.getContentType(format); FieldSearchResult result = null; if (wantedFields.length > 0 || sessionToken != null) { if (sessionToken != null) { result = m_access.resumeFindObjects(context, sessionToken); } else { if ((terms != null) && (terms.length() != 0)) { result = m_access.findObjects(context, wantedFields, maxResults, new FieldSearchQuery(terms)); } else { result = m_access.findObjects(context, wantedFields, maxResults, new FieldSearchQuery(Condition.getConditions(query))); } } } ReadableCharArrayWriter writer = new ReadableCharArrayWriter(2048); if (TEXT_HTML.isCompatible(mime)) { getSerializer(context).searchResultToHtml( query, terms, SEARCHABLE_FIELDS, wantedFields, maxResults, result, writer); writer.close(); } else { getSerializer(context).searchResultToXml(result, writer); writer.close(); } return Response.ok(writer.toReader(), mime).build(); } catch (Exception ex) { return handleException(ex, flash); } } /** * Implements the "getNextPID" functionality of the Fedora Management LITE * (API-M-LITE) interface using a java servlet front end. The syntax defined * by API-M-LITE for getting a list of the next available PIDs has the * following binding: * <ul> * <li>getNextPID URL syntax: * protocol://hostname:port/fedora/objects/nextPID[?numPIDs=NUMPIDS&namespace=NAMESPACE&format=html,xml] * This syntax requests a list of next available PIDS. The parameter numPIDs * determines the number of requested PIDS to generate. If omitted, numPIDs * defaults to 1. The namespace parameter determines the namespace to be * used in generating the PIDs. If omitted, namespace defaults to the * namespace defined in the fedora.fcfg configuration file for the parameter * pidNamespace. The xml parameter determines the type of output returned. * If the parameter is omitted or has a value of "false", a MIME-typed * stream consisting of an html table is returned providing a browser-savvy * means of viewing the object profile. If the value specified is "true", * then a MIME-typed stream consisting of XML is returned.</li> * </ul> */ @Path("/nextPID") @POST public Response getNextPID( @QueryParam("numPIDs") @DefaultValue("1") int numPIDS, @QueryParam(RestParam.NAMESPACE) String namespace, @QueryParam("format") @DefaultValue(HTML) String format, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) throws Exception { try { Context context = getContext(); String[] pidList = m_management.getNextPID(context, numPIDS, namespace); MediaType mime = RestHelper.getContentType(format); if (pidList.length > 0) { ReadableCharArrayWriter xml = new ReadableCharArrayWriter(512); DefaultSerializer.pidsToXml(pidList, xml); xml.close(); if (TEXT_HTML.isCompatible(mime)) { Reader reader = xml.toReader(); xml = new ReadableCharArrayWriter(512); transform(reader, "management/getNextPIDInfo.xslt", xml); xml.close(); } return Response.ok(xml.toReader(), mime).build(); } else { return Response.noContent().build(); } } catch (Exception ex) { return handleException(ex, flash); } } private static String[] getWantedFields( HttpServletRequest request) { List<String> fields = new ArrayList<String>(); for (String f : SEARCHABLE_FIELDS) { if ("true".equals(request.getParameter(f))) { fields.add(f); } } return fields.toArray(new String[fields.size()]); } @Path(VALID_PID_PART +"/validate") @GET @Produces({XML}) public Response doObjectValidation( @PathParam(RestParam.PID) String pid, @QueryParam(RestParam.AS_OF_DATE_TIME) String dateTime, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); Date asOfDateTime = DateUtility.parseDateOrNull(dateTime); MediaType mediaType = TEXT_XML; Validation validation = m_management.validate(context, pid, asOfDateTime); ReadableCharArrayWriter xml = new ReadableCharArrayWriter(1024); DefaultSerializer.objectValidationToXml(validation, xml); xml.close(); return Response.ok(xml.toReader(), mediaType).build(); } catch (Exception ex) { return handleException(ex, flash); } } /** * Exports the entire digital object in the specified XML format * ("info:fedora/fedora-system:FOXML-1.1" or * "info:fedora/fedora-system:METSFedoraExt-1.1"), and encoded appropriately * for the specified export context ("public", "migrate", or "archive"). * <br> * GET /objects/{pid}/export ? format context encoding */ @Path(VALID_PID_PART + "/export") @GET @Produces({XML, ZIP}) public Response getObjectExport( @PathParam(RestParam.PID) String pid, @QueryParam(RestParam.FORMAT) @DefaultValue(FOXML1_1) String format, @QueryParam(RestParam.EXPORT_CONTEXT) String exportContext, @QueryParam(RestParam.ENCODING) @DefaultValue(DEFAULT_ENC) String encoding, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); StreamingOutput is = m_management.stream(context, pid, format, exportContext, encoding); MediaType mediaType = TEXT_XML; if (format.equals(ATOMZIP1_1)) { mediaType = MediaType.valueOf(ZIP); } return Response.ok(is, mediaType).build(); } catch (Exception ex) { return handleException(ex, flash); } } /** * Gets a list of timestamps indicating when components changed in an * object. This is a set of timestamps indicating when a datastream or * disseminator was created or modified in the object. These timestamps can * be used to request a timestamped dissemination request to view the object * as it appeared at a specific point in time. * <br> * GET /objects/{pid}/versions ? format */ @Path(VALID_PID_PART + "/versions") @GET public Response getObjectHistory( @PathParam(RestParam.PID) String pid, @QueryParam(RestParam.FORMAT) @DefaultValue(HTML) String format, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); String[] objectHistory = m_access.getObjectHistory(context, pid); ReadableCharArrayWriter xml = new ReadableCharArrayWriter(1024); DefaultSerializer.objectHistoryToXml(objectHistory, pid, xml); xml.close(); MediaType mime = RestHelper.getContentType(format); if (TEXT_HTML.isCompatible(mime)) { Reader reader = xml.toReader(); xml = new ReadableCharArrayWriter(1024); transform(reader, "access/viewObjectHistory.xslt", xml); xml.close(); } return Response.ok(xml.toReader(), mime).build(); } catch (Exception ex) { return handleException(ex, flash); } } /** * Gets a profile of the object which includes key metadata fields and URLs * for the object Dissemination Index and the object Item Index. Can be * thought of as a default view of the object. * <br> * GET /objects/{pid}/objectXML */ @Path(VALID_PID_PART + "/objectXML") @GET @Produces(XML) public Response getObjectXML( @PathParam(RestParam.PID) String pid, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); InputStream is = m_management.getObjectXML(context, pid, DEFAULT_ENC); return Response.ok(is, TEXT_XML).build(); } catch (Exception ex) { return handleException(ex, flash); } } /** * Gets a profile of the object which includes key metadata fields and URLs * for the object Dissemination Index and the object Item Index. Can be * thought of as a default view of the object. * <br> * GET /objects/{pid} ? format asOfDateTime */ @GET @Produces({HTML, XML}) @Path(VALID_PID_PART) public Response getObjectProfile( @PathParam(RestParam.PID) String pid, @QueryParam(RestParam.AS_OF_DATE_TIME) String dateTime, @QueryParam(RestParam.FORMAT) @DefaultValue(HTML) String format, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Date asOfDateTime = DateUtility.parseDateOrNull(dateTime); Context context = getContext(); ObjectProfile objProfile = m_access.getObjectProfile(context, pid, asOfDateTime); ReadableCharArrayWriter out = new ReadableCharArrayWriter(1024); DefaultSerializer.objectProfileToXML(objProfile, asOfDateTime, out); out.close(); MediaType mime = RestHelper.getContentType(format); if (TEXT_HTML.isCompatible(mime)) { Reader reader = out.toReader(); out = new ReadableCharArrayWriter(1024); transform(reader, "access/viewObjectProfile.xslt", out); out.close(); } return Response.ok(out.toReader(), mime).build(); } catch (Exception ex) { return handleException(ex, flash); } } /** * Permanently removes an object from the repository. * <br> * DELETE /objects/{pid} ? logMessage */ @DELETE @Path(VALID_PID_PART) public Response deleteObject( @PathParam(RestParam.PID) String pid, @QueryParam("logMessage") String logMessage, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); Date d = m_management.purgeObject(context, pid, logMessage); return Response.ok(DateUtility.convertDateToXSDString(d), MediaType.TEXT_PLAIN_TYPE).build(); } catch (Exception ex) { return handleException(ex, flash); } } @POST @Path("/new") @Consumes({XML, FORM}) public Response newObject( @javax.ws.rs.core.Context HttpHeaders headers, @QueryParam(RestParam.LABEL) String label, @QueryParam(RestParam.LOG_MESSAGE) String logMessage, @QueryParam(RestParam.FORMAT) @DefaultValue(FOXML1_1) String format, @QueryParam(RestParam.ENCODING) @DefaultValue(DEFAULT_ENC) String encoding, @QueryParam(RestParam.NAMESPACE) String namespace, @QueryParam(RestParam.OWNER_ID) String ownerID, @QueryParam(RestParam.STATE) @DefaultValue("A") String state, @QueryParam(RestParam.IGNORE_MIME) @DefaultValue("false") boolean ignoreMime, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { return createObject(headers, "new", label, logMessage, format, encoding, namespace, ownerID, state, ignoreMime, flash); } /** * Create/Update a new digital object. If no xml given in the body, will * create an empty object. * <br> * POST /objects/{pid} ? label logMessage format encoding namespace ownerId state */ @POST @Path(VALID_PID_PART) @Consumes({XML, FORM, ZIP}) public Response createObject( @javax.ws.rs.core.Context HttpHeaders headers, @PathParam(RestParam.PID) String pid, @QueryParam(RestParam.LABEL) String label, @QueryParam(RestParam.LOG_MESSAGE) String logMessage, @QueryParam(RestParam.FORMAT) @DefaultValue(FOXML1_1) String format, @QueryParam(RestParam.ENCODING) @DefaultValue(DEFAULT_ENC) String encoding, @QueryParam(RestParam.NAMESPACE) String namespace, @QueryParam(RestParam.OWNER_ID) String ownerID, @QueryParam(RestParam.STATE) @DefaultValue("A") String state, @QueryParam(RestParam.IGNORE_MIME) @DefaultValue("false") boolean ignoreMime, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); InputStream is = null; // Determine if content is provided RequestContent content = RestUtil.getRequestContent(m_servletRequest, headers); if (content != null && content.getContentStream() != null) { if (ignoreMime) { is = content.getContentStream(); } else { // Make sure content is XML or ZIP String contentMime = content.getMimeType(); if (contentMime != null) { MediaType t = MediaType.valueOf(contentMime); if (TEXT_XML.isCompatible(t) || APP_ZIP.isCompatible(t)) { is = content.getContentStream(); } } } } // If no content is provided, use a FOXML template if (is == null) { if (pid == null || pid.equals("new")) { pid = m_management.getNextPID(context, 1, namespace)[0]; } if (ownerID == null || ownerID.trim().isEmpty()) { ownerID = context.getSubjectValue(Constants.SUBJECT.LOGIN_ID.uri); } ReadableByteArrayOutputStream bytes = new ReadableByteArrayOutputStream(1024); PrintWriter xml = new PrintWriter( new OutputStreamWriter(bytes, Charset.forName(encoding))); char stateChar = state.trim().toUpperCase().charAt(0); getFOXMLTemplate(pid, label, ownerID, stateChar, encoding, xml); xml.close(); is = bytes.toInputStream(); } else { if (namespace != null && !namespace.isEmpty()) { logger.warn("The namespace parameter is only applicable whene object " + "content is not provided, thus the namespace provided '" + namespace + "' has been ignored."); } } pid = m_management.ingest(context, is, logMessage, format, encoding, pid); URI createdLocation = m_uriInfo.getRequestUri().resolve(URLEncoder.encode(pid, DEFAULT_ENC)); return Response.created(createdLocation).entity(pid).build(); } catch (Exception ex) { return handleException(ex, flash); } } /** * Update (modify) digital object. * <p>PUT /objects/{pid} ? label logMessage ownerId state lastModifiedDate</p> * * @param pid the persistent identifier * @param label * @param logMessage * @param ownerID * @param state * @param lastModifiedDate Optional XSD dateTime to guard against concurrent * modification. If provided (i.e. not null), the request will fail with an * HTTP 409 Conflict if lastModifiedDate is earlier than the object's * lastModifiedDate. * @return The timestamp for this modification (as an XSD dateTime string) * @see org.fcrepo.server.management.Management#modifyObject(org.fcrepo.server.Context, String, String, String, String, String, Date) */ @PUT @Path(VALID_PID_PART) @Produces(MediaType.TEXT_PLAIN) public Response updateObject( @PathParam(RestParam.PID) String pid, @QueryParam(RestParam.LABEL) String label, @QueryParam(RestParam.LOG_MESSAGE) String logMessage, @QueryParam(RestParam.OWNER_ID) String ownerID, @QueryParam(RestParam.STATE) String state, @QueryParam(RestParam.LAST_MODIFIED_DATE) DateTimeParam lastModifiedDate, @QueryParam(RestParam.FLASH) @DefaultValue("false") boolean flash) { try { Context context = getContext(); Date requestModDate = null; if (lastModifiedDate != null) { requestModDate = lastModifiedDate.getValue(); } Date lastModDate = m_management.modifyObject(context, pid, state, label, ownerID, logMessage, requestModDate); return Response.ok().entity(DateUtility.convertDateToXSDString(lastModDate)).build(); } catch (Exception ex) { return handleException(ex, flash); } } private static void getFOXMLTemplate( String pid, String label, String ownerId, char state, String encoding, Writer xml) throws IOException { xml.append("<?xml version=\"1.0\" encoding=\""); xml.append(encoding); xml.append("\"?>\n" + "<foxml:digitalObject VERSION=\"1.1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + " xmlns:foxml=\"info:fedora/fedora-system:def/foxml#\"\n" + " xsi:schemaLocation=\""); xml.append(Constants.FOXML.uri); xml.append(' '); xml.append(Constants.FOXML1_1.xsdLocation); xml.append('"'); if (pid != null && pid.length() > 0) { xml.append("\n PID=\""); StreamUtility.enc(pid, xml); xml.append('"'); } xml.append(">\n <foxml:objectProperties>\n" + " <foxml:property NAME=\"info:fedora/fedora-system:def/model#state\" VALUE=\""); xml.append(state); xml.append("\"/>\n" + " <foxml:property NAME=\"info:fedora/fedora-system:def/model#label\" VALUE=\""); StreamUtility.enc(label, xml); xml.append("\"/>\n" + " <foxml:property NAME=\"info:fedora/fedora-system:def/model#ownerId\" VALUE=\""); xml.append(ownerId); xml.append("\"/>\n </foxml:objectProperties>\n</foxml:digitalObject>"); } }