/* * ContainerAdapter.java * * Version: $Revision: 4265 $ * * Date: $Date: 2009-09-16 09:16:58 +0000 (Wed, 16 Sep 2009) $ * * Copyright (c) 2002-2005, Hewlett-Packard Company and Massachusetts * Institute of Technology. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of the Hewlett-Packard Company nor the name of the * Massachusetts Institute of Technology nor the names of their * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.dspace.app.xmlui.objectmanager; import java.io.ByteArrayInputStream; import java.io.IOException; import java.sql.SQLException; import org.dspace.app.xmlui.wing.AttributeMap; import org.dspace.app.xmlui.wing.WingException; import org.dspace.authorize.AuthorizeException; import org.dspace.browse.ItemCounter; import org.dspace.browse.ItemCountException; import org.dspace.content.Bitstream; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.crosswalk.CrosswalkException; import org.dspace.content.crosswalk.DisseminationCrosswalk; import org.dspace.core.ConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import org.jdom.output.SAXOutputter; import org.xml.sax.SAXException; /** * This is an adapter which translates DSpace containers * (communities & collections) into METS documents. This adapter follows * the DSpace METS profile however that profile does not define how a * community or collection should be described, but we make the obvious * decisions to deviate when nessasary from the profile. * * The METS document consists of three parts: descriptive metadata section, * file section, and a structural map. The descriptive metadata sections holds * metadata about the item being adapted using DSpace crosswalks. This is the * same way the item adapter works. * * However the file section and structural map are a bit different. In these * casses the the only files listed is the one logo that may be attached to * a community or collection. * * @author Scott Phillips */ public class ContainerAdapter extends AbstractAdapter { /** The community or collection this adapter represents. */ private DSpaceObject dso; /** A space seperated list of descriptive metadata sections */ private StringBuffer dmdSecIDS; /** Current DSpace context **/ private Context dspaceContext; /** * Construct a new CommunityCollectionMETSAdapter. * * @param dso * A DSpace Community or Collection to adapt. * @param contextPath * The contextPath of this webapplication. */ public ContainerAdapter(Context context, DSpaceObject dso,String contextPath) { super(contextPath); this.dso = dso; this.dspaceContext = context; } /** Return the container, community or collection, object */ public DSpaceObject getContainer() { return this.dso; } /** * * * * Required abstract methods * * * */ /** * Return the URL of this community/collection in the interface */ protected String getMETSOBJID() { if (dso.getHandle() != null) return contextPath+"/handle/" + dso.getHandle(); return null; } /** * @return Return the URL for editing this item */ protected String getMETSOBJEDIT() { return null; } /** * Use the handle as the id for this METS document */ protected String getMETSID() { if (dso.getHandle() == null) { if (dso instanceof Collection) return "collection:"+dso.getID(); else return "community:"+dso.getID(); } else return "hdl:"+dso.getHandle(); } /** * Return the profile to use for communities and collections. * */ protected String getMETSProfile() throws WingException { return "DSPACE METS SIP Profile 1.0"; } /** * Return a friendly label for the METS document to say we are a community * or collection. */ protected String getMETSLabel() { if (dso instanceof Community) return "DSpace Community"; else return "DSpace Collection"; } /** * Return a unique id for the given bitstream */ protected String getFileID(Bitstream bitstream) { return "file_" + bitstream.getID(); } /** * Return a group id for the given bitstream */ protected String getGroupFileID(Bitstream bitstream) { return "group_file_" + bitstream.getID(); } /** * * * * METS structural methods * * * */ /** * Render the METS descriptive section. This will create a new metadata * section for each crosswalk configured. * * Example: * <dmdSec> * <mdWrap MDTYPE="MODS"> * <xmlData> * ... content from the crosswalk ... * </xmlDate> * </mdWrap> * </dmdSec */ protected void renderDescriptiveSection() throws WingException, SAXException, CrosswalkException, IOException, SQLException { AttributeMap attributes; String groupID = getGenericID("group_dmd_"); dmdSecIDS = new StringBuffer(); // Add DIM descriptive metadata if it was requested or if no metadata types // were specified. Further more since this is the default type we also use a // faster rendering method that the crosswalk API. if(dmdTypes.size() == 0 || dmdTypes.contains("DIM")) { // Metadata element's ID String dmdID = getGenericID("dmd_"); // Keep track of all descriptive sections dmdSecIDS.append(dmdID); // //////////////////////////////// // Start a new dmdSec for each crosswalk. attributes = new AttributeMap(); attributes.put("ID", dmdID); attributes.put("GROUPID", groupID); startElement(METS,"dmdSec",attributes); // //////////////////////////////// // Start metadata wrapper attributes = new AttributeMap(); attributes.put("MDTYPE", "OTHER"); attributes.put("OTHERMDTYPE", "DIM"); startElement(METS,"mdWrap",attributes); // //////////////////////////////// // Start the xml data startElement(METS,"xmlData"); // /////////////////////////////// // Start the DIM element attributes = new AttributeMap(); attributes.put("dspaceType", Constants.typeText[dso.getType()]); startElement(DIM,"dim",attributes); // Add each field for this collection if (dso.getType() == Constants.COLLECTION) { Collection collection = (Collection) dso; String description = collection.getMetadata("introductory_text"); String description_abstract = collection.getMetadata("short_description"); String description_table = collection.getMetadata("side_bar_text"); String identifier_uri = "http://hdl.handle.net/" + collection.getHandle(); String provenance = collection.getMetadata("provenance_description"); String rights = collection.getMetadata("copyright_text"); String rights_license = collection.getMetadata("license"); String title = collection.getMetadata("name"); createField("dc","description",null,null,description); createField("dc","description","abstract",null,description_abstract); createField("dc","description","tableofcontents",null,description_table); createField("dc","identifier","uri",null,identifier_uri); createField("dc","provenance",null,null,provenance); createField("dc","rights",null,null,rights); createField("dc","rights","license",null,rights_license); createField("dc","title",null,null,title); boolean useCache = ConfigurationManager.getBooleanProperty("webui.strengths.cache"); //To improve scalability, XMLUI only adds item counts if they are cached if (useCache) { try { //try to determine Collection size (i.e. # of items) int size = new ItemCounter(this.dspaceContext).getCount(collection); createField("dc","format","extent",null, String.valueOf(size)); } catch(ItemCountException e) { IOException ioe = new IOException("Could not obtain Collection item-count"); ioe.initCause(e); throw ioe; } } } else if (dso.getType() == Constants.COMMUNITY) { Community community = (Community) dso; String description = community.getMetadata("introductory_text"); String description_abstract = community.getMetadata("short_description"); String description_table = community.getMetadata("side_bar_text"); String identifier_uri = "http://hdl.handle.net/" + community.getHandle(); String rights = community.getMetadata("copyright_text"); String title = community.getMetadata("name"); createField("dc","description",null,null,description); createField("dc","description","abstract",null,description_abstract); createField("dc","description","tableofcontents",null,description_table); createField("dc","identifier","uri",null,identifier_uri); createField("dc","rights",null,null,rights); createField("dc","title",null,null,title); boolean useCache = ConfigurationManager.getBooleanProperty("webui.strengths.cache"); //To improve scalability, XMLUI only adds item counts if they are cached if (useCache) { try { //try to determine Community size (i.e. # of items) int size = new ItemCounter(this.dspaceContext).getCount(community); createField("dc","format","extent",null, String.valueOf(size)); } catch(ItemCountException e) { IOException ioe = new IOException("Could not obtain Collection item-count"); ioe.initCause(e); throw ioe; } } } // /////////////////////////////// // End the DIM element endElement(DIM,"dim"); // //////////////////////////////// // End elements endElement(METS,"xmlData"); endElement(METS,"mdWrap"); endElement(METS, "dmdSec"); } for (String dmdType : dmdTypes) { // If DIM was requested then it was generated above without using // the crosswalk API. So we can skip this one. if ("DIM".equals(dmdType)) continue; DisseminationCrosswalk crosswalk = getDisseminationCrosswalk(dmdType); if (crosswalk == null) continue; String dmdID = getGenericID("dmd_"); // Add our id to the list. dmdSecIDS.append(" " + dmdID); // //////////////////////////////// // Start a new dmdSec for each crosswalk. attributes = new AttributeMap(); attributes.put("ID", dmdID); attributes.put("GROUPID", groupID); startElement(METS,"dmdSec",attributes); // //////////////////////////////// // Start metadata wrapper attributes = new AttributeMap(); if (isDefinedMETStype(dmdType)) { attributes.put("MDTYPE", dmdType); } else { attributes.put("MDTYPE", "OTHER"); attributes.put("OTHERMDTYPE", dmdType); } startElement(METS,"mdWrap",attributes); // //////////////////////////////// // Start the xml data startElement(METS,"xmlData"); // /////////////////////////////// // Send the actual XML content try { Element dissemination = crosswalk.disseminateElement(dso); SAXFilter filter = new SAXFilter(contentHandler, lexicalHandler, namespaces); // Allow the basics for XML filter.allowElements().allowIgnorableWhitespace().allowCharacters().allowCDATA().allowPrefixMappings(); SAXOutputter outputter = new SAXOutputter(); outputter.setContentHandler(filter); outputter.setLexicalHandler(filter); outputter.output(dissemination); } catch (JDOMException jdome) { throw new WingException(jdome); } catch (AuthorizeException ae) { // just ignore the authorize exception and continue on with //out parsing the xml document. } // //////////////////////////////// // End elements endElement(METS,"xmlData"); endElement(METS,"mdWrap"); endElement(METS, "dmdSec"); // Record keeping if (dmdSecIDS == null) { dmdSecIDS = new StringBuffer(dmdID); } else { dmdSecIDS.append(" " + dmdID); } } } /** * Render the METS file section. If a logo is present for this * container then that single bitstream is listed in the * file section. * * Example: * <fileSec> * <fileGrp USE="LOGO"> * <file ... > * <fLocate ... > * </file> * </fileGrp> * </fileSec> */ protected void renderFileSection() throws SAXException { AttributeMap attributes; // Get the Community or Collection logo. Bitstream logo = getLogo(); if (logo != null) { // //////////////////////////////// // Start the file section startElement(METS,"fileSec"); // //////////////////////////////// // Start a new fileGrp for the logo. attributes = new AttributeMap(); attributes.put("USE", "LOGO"); startElement(METS,"fileGrp",attributes); // //////////////////////////////// // Add the actual file element String fileID = getFileID(logo); String groupID = getGroupFileID(logo); renderFile(null, logo, fileID, groupID); // //////////////////////////////// // End th file group and file section endElement(METS,"fileGrp"); endElement(METS,"fileSec"); } } /** * Render the container's structural map. This includes a refrence * to the container's logo, if available, otherwise it is an empty * division that just states it is a DSpace community or Collection. * * Examlpe: * <structMap TYPE="LOGICAL" LABEL="DSpace"> * <div TYPE="DSpace Collection" DMDID="space seperated list of ids"> * <fptr FILEID="logo id"/> * </div> * </structMap> */ protected void renderStructureMap() throws SQLException, SAXException { AttributeMap attributes; // /////////////////////// // Start a new structure map attributes = new AttributeMap(); attributes.put("TYPE", "LOGICAL"); attributes.put("LABEL", "DSpace"); startElement(METS,"structMap",attributes); // //////////////////////////////// // Start the special first division attributes = new AttributeMap(); attributes.put("TYPE", getMETSLabel()); // add references to the Descriptive metadata if (dmdSecIDS != null) attributes.put("DMDID", dmdSecIDS.toString()); startElement(METS,"div",attributes); // add a fptr pointer to the logo. Bitstream logo = getLogo(); if (logo != null) { // //////////////////////////////// // Add a refrence to the logo as the primary bitstream. attributes = new AttributeMap(); attributes.put("FILEID",getFileID(logo)); startElement(METS,"fptr",attributes); endElement(METS,"fptr"); // /////////////////////////////////////////////// // Add a div for the publicaly viewable bitstreams (i.e. the logo) attributes = new AttributeMap(); attributes.put("ID", getGenericID("div_")); attributes.put("TYPE", "DSpace Content Bitstream"); startElement(METS,"div",attributes); // //////////////////////////////// // Add a refrence to the logo as the primary bitstream. attributes = new AttributeMap(); attributes.put("FILEID",getFileID(logo)); startElement(METS,"fptr",attributes); endElement(METS,"fptr"); // ////////////////////////// // End the logo division endElement(METS,"div"); } // //////////////////////////////// // End the special first division endElement(METS,"div"); // /////////////////////// // End the structure map endElement(METS,"structMap"); } /** * * * * Private helpfull methods * * * */ /** * Return the logo bitstream associated with this community or collection. * If there is no logo then null is returned. */ private Bitstream getLogo() { if (dso instanceof Community) { Community community = (Community) dso; return community.getLogo(); } else if (dso instanceof Collection) { Collection collection = (Collection) dso; return collection.getLogo(); } return null; } /** * Count how many occurance there is of the given * character in the given string. * * @param string The string value to be counted. * @param character the character to count in the string. */ private int countOccurances(String string, char character) { if (string == null || string.length() == 0) return 0; int fromIndex = -1; int count = 0; while (true) { fromIndex = string.indexOf('>', fromIndex+1); if (fromIndex == -1) break; count++; } return count; } /** * Check if the given character sequence is located in the given * string at the specified index. If it is then return true, otherwise false. * * @param string The string to test against * @param index The location within the string * @param characters The character sequence to look for. * @return true if the character sequence was found, otherwise false. */ private boolean substringCompare(String string, int index, char ... characters) { // Is the string long enough? if (string.length() <= index + characters.length) return false; // Do all the characters match? for (char character : characters) { if (string.charAt(index) != character) return false; index++; } return false; } /** * Create a new DIM field element with the given attributes. * * @param schema The schema the DIM field belongs too. * @param element The element the DIM field belongs too. * @param qualifier The qualifier the DIM field belongs too. * @param language The language the DIM field belongs too. * @param value The value of the DIM field. * @return A new DIM field element * @throws SAXException */ private void createField(String schema, String element, String qualifier, String language, String value) throws SAXException { // /////////////////////////////// // Field element for each metadata field. AttributeMap attributes = new AttributeMap(); attributes.put("mdschema",schema); attributes.put("element", element); if (qualifier != null) attributes.put("qualifier", qualifier); if (language != null) attributes.put("language", language); startElement(DIM,"field",attributes); // Only try and add the metadata's value, but only if it is non null. if (value != null) { // First, preform a queck check to see if the value may be XML. int countOpen = countOccurances(value,'<'); int countClose = countOccurances(value, '>'); // If it passed the quick test, then try and parse the value. Element xmlDocument = null; if (countOpen > 0 && countOpen == countClose) { // This may be XML, First try and remove any bad entity refrences. int amp = -1; while ((amp = value.indexOf('&', amp+1)) > -1) { // Is it an xml entity named by number? if (substringCompare(value,amp+1,'#')) continue; // & if (substringCompare(value,amp+1,'a','m','p',';')) continue; // ' if (substringCompare(value,amp+1,'a','p','o','s',';')) continue; // " if (substringCompare(value,amp+1,'q','u','o','t',';')) continue; // < if (substringCompare(value,amp+1,'l','t',';')) continue; // > if (substringCompare(value,amp+1,'g','t',';')) continue; // Replace the ampersand with an XML entity. value = value.substring(0,amp) + "&" + value.substring(amp+1); } // Second try and parse the XML into a mini-dom try { // Wrap the value inside a root element (which will be trimed out // by the SAX filter and set the default namespace to XHTML. String xml = "<?xml version='1.0' encoding='UTF-8'?><fragment xmlns=\"http://www.w3.org/1999/xhtml\">"+value+"</fragment>"; ByteArrayInputStream inputStream = new ByteArrayInputStream(xml.getBytes("UTF-8")); SAXBuilder builder = new SAXBuilder(); Document document = builder.build(inputStream); xmlDocument = document.getRootElement(); } catch (Exception e) { // ignore any errors we get, and just add the string literaly. } } // Third, If we have xml, attempt to serialize the dom. if (xmlDocument != null) { SAXFilter filter = new SAXFilter(contentHandler, lexicalHandler, namespaces); // Allow the basics for XML filter.allowElements().allowIgnorableWhitespace().allowCharacters().allowCDATA().allowPrefixMappings(); // Special option, only allow elements below the second level to pass through. This // will trim out the METS declaration and only leave the actual METS parts to be // included. filter.allowElements(1); SAXOutputter outputter = new SAXOutputter(); outputter.setContentHandler(filter); outputter.setLexicalHandler(filter); try { outputter.output(xmlDocument); } catch (JDOMException jdome) { // serialization failed so let's just fallback sending the plain characters. sendCharacters(value); } } else { // We don't have XML, so just send the plain old characters. sendCharacters(value); } } // ////////////////////////////// // Close out field endElement(DIM,"field"); } }