/* * AbstractMETSDisseminator.java * * Version: $Revision: 3761 $ * * Date: $Date: 2009-05-07 04:18:02 +0000 (Thu, 07 May 2009) $ * * Copyright (c) 2002-2009, The DSpace Foundation. 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 DSpace Foundation nor the names of its * 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.content.packager; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeManager; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; 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.dspace.core.PluginManager; import org.dspace.core.Utils; import org.jdom.Namespace; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; import edu.harvard.hul.ois.mets.Agent; import edu.harvard.hul.ois.mets.AmdSec; import edu.harvard.hul.ois.mets.Checksumtype; import edu.harvard.hul.ois.mets.Div; import edu.harvard.hul.ois.mets.DmdSec; import edu.harvard.hul.ois.mets.FLocat; import edu.harvard.hul.ois.mets.FileGrp; import edu.harvard.hul.ois.mets.FileSec; import edu.harvard.hul.ois.mets.Fptr; import edu.harvard.hul.ois.mets.Loctype; import edu.harvard.hul.ois.mets.MdWrap; import edu.harvard.hul.ois.mets.Mdtype; import edu.harvard.hul.ois.mets.Mets; import edu.harvard.hul.ois.mets.MetsHdr; import edu.harvard.hul.ois.mets.Name; import edu.harvard.hul.ois.mets.Role; import edu.harvard.hul.ois.mets.StructMap; import edu.harvard.hul.ois.mets.TechMD; import edu.harvard.hul.ois.mets.Type; import edu.harvard.hul.ois.mets.XmlData; import edu.harvard.hul.ois.mets.helper.MetsElement; import edu.harvard.hul.ois.mets.helper.MetsException; import edu.harvard.hul.ois.mets.helper.MetsValidator; import edu.harvard.hul.ois.mets.helper.MetsWriter; import edu.harvard.hul.ois.mets.helper.PCData; import edu.harvard.hul.ois.mets.helper.PreformedXML; /** * Base class for disseminator of * METS (Metadata Encoding & Transmission Standard) Package.<br> * See <a href="http://www.loc.gov/standards/mets/">http://www.loc.gov/standards/mets/</a> * <p> * This is a generic packager framework intended to be subclassed to create * packagers for more specific METS "profiles". METS is an * abstract and flexible framework that can encompass many * different kinds of metadata and inner package structures. * <p> * <b>Package Parameters:</b><br> * <code>manifestOnly</code> -- if true, generate a standalone XML * document of the METS manifest instead of a complete package. Any * other metadata (such as licenses) will be encoded inline. * Default is <code>false</code>. * * <code>unauthorized</code> -- this determines what is done when the * packager encounters a Bundle or Bitstream it is not authorized to * read. By default, it just quits with an AuthorizeException. * If this option is present, it must be one of the following values: * <code>skip</code> -- simply exclude unreadable content from package. * <code>zero</code> -- include unreadable bitstreams as 0-length files; * unreadable Bundles will still cause authorize errors. * * @author Larry Stone * @author Robert Tansley * @version $Revision: 3761 $ */ public abstract class AbstractMETSDisseminator implements PackageDisseminator { /** log4j category */ private static Logger log = Logger.getLogger(AbstractMETSDisseminator.class); /** Filename of manifest, relative to package toplevel. */ public static final String MANIFEST_FILE = "mets.xml"; // JDOM xml output writer - indented format for readability. private static XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat()); // for gensym() private int idCounter = 1; /** * Table of files to add to package, such as mdRef'd metadata. * Key is relative pathname of file, value is <code>InputStream</code> * with contents to put in it. * New map is created by disseminate(). */ protected Map extraFiles = null; /** * Make a new unique ID with specified prefix. * @param prefix the prefix of the identifier, constrained to XML ID schema * @return a new string identifier unique in this session (instance). */ protected String gensym(String prefix) { return prefix + "_" + String.valueOf(idCounter++); } public String getMIMEType(PackageParameters params) { return (params != null && params.getProperty("manifestOnly") != null) ? "text/xml" : "application/zip"; } /** * Export the object (Item, Collection, or Community) to a * package file on the indicated OutputStream. * Gets an exception of the object cannot be packaged or there is * a failure creating the package. * * @param context - DSpace context. * @param dso - DSpace object (item, collection, etc) * @param pkg - output stream on which to write package * @throws PackageException if package cannot be created or there is * a fatal error in creating it. */ public void disseminate(Context context, DSpaceObject dso, PackageParameters params, OutputStream pkg) throws PackageValidationException, CrosswalkException, AuthorizeException, SQLException, IOException { if (dso.getType() == Constants.ITEM) { Item item = (Item)dso; long lmTime = item.getLastModified().getTime(); // how to handle unauthorized bundle/bitstream: String unauth = (params == null) ? null : params.getProperty("unauthorized"); if (params != null && params.getProperty("manifestOnly") != null) { extraFiles = null; writeManifest(context, item, params, pkg); } else { extraFiles = new HashMap(); ZipOutputStream zip = new ZipOutputStream(pkg); zip.setComment("METS archive created by DSpace METSDisseminationCrosswalk"); // write manifest first. ZipEntry me = new ZipEntry(MANIFEST_FILE); me.setTime(lmTime); zip.putNextEntry(me); writeManifest(context, item, params, zip); zip.closeEntry(); // copy extra (meta?) bitstreams into zip Iterator fi = extraFiles.keySet().iterator(); while (fi.hasNext()) { String fname = (String)fi.next(); ZipEntry ze = new ZipEntry(fname); ze.setTime(lmTime); zip.putNextEntry(ze); Utils.copy((InputStream)extraFiles.get(fname), zip); zip.closeEntry(); } // copy all non-meta bitstreams into zip Bundle bundles[] = item.getBundles(); for (int i = 0; i < bundles.length; i++) { if (!PackageUtils.isMetaInfoBundle(bundles[i])) { // unauthorized bundle? if (!AuthorizeManager.authorizeActionBoolean(context, bundles[i], Constants.READ)) { if (unauth != null && (unauth.equalsIgnoreCase("skip"))) { log.warn("Skipping Bundle[\""+bundles[i].getName()+"\"] because you are not authorized to read it."); continue; } else throw new AuthorizeException("Not authorized to read Bundle named \""+bundles[i].getName()+"\""); } Bitstream[] bitstreams = bundles[i].getBitstreams(); for (int k = 0; k < bitstreams.length; k++) { boolean auth = AuthorizeManager.authorizeActionBoolean(context, bitstreams[k], Constants.READ); if (auth || (unauth != null && unauth.equalsIgnoreCase("zero"))) { ZipEntry ze = new ZipEntry( makeBitstreamName(bitstreams[k])); ze.setTime(lmTime); ze.setSize(auth ? bitstreams[k].getSize() : 0); zip.putNextEntry(ze); if (auth) Utils.copy(bitstreams[k].retrieve(), zip); else log.warn("Adding zero-length file for Bitstream, SID="+String.valueOf(bitstreams[k].getSequenceID())+", not authorized for READ."); zip.closeEntry(); } else if (unauth != null && unauth.equalsIgnoreCase("skip")) { log.warn("Skipping Bitstream, SID="+String.valueOf(bitstreams[k].getSequenceID())+", not authorized for READ."); } else { throw new AuthorizeException("Not authorized to read Bitstream, SID="+String.valueOf(bitstreams[k].getSequenceID())); } } } } zip.close(); extraFiles = null; } } else throw new PackageValidationException("Can only disseminate an Item now."); } /** * Create name that bitstream will have in archive. Name must * be unique and relative to archive top level, e.g. "bitstream_<id>.ext" */ private String makeBitstreamName(Bitstream bitstream) { String base = "bitstream_"+String.valueOf(bitstream.getID()); String ext[] = bitstream.getFormat().getExtensions(); return (ext.length > 0) ? base+"."+ext[0] : base; } // set metadata type - if Mdtype.parse() gets exception, // that means it's not in the MDTYPE vocabulary, so use OTHER. private void setMdType(MdWrap mdWrap, String mdtype) { try { mdWrap.setMDTYPE(Mdtype.parse(mdtype)); } catch (MetsException e) { mdWrap.setMDTYPE(Mdtype.OTHER); mdWrap.setOTHERMDTYPE(mdtype); } } /** * Write out a METS manifest. * Mostly lifted from Rob Tansley's METS exporter. */ private void writeManifest(Context context, Item item, PackageParameters params, OutputStream out) throws PackageValidationException, CrosswalkException, AuthorizeException, SQLException, IOException { try { // Create the METS file Mets mets = new Mets(); // Top-level stuff mets.setID(gensym("mets")); mets.setOBJID("hdl:" + item.getHandle()); mets.setLABEL("DSpace Item"); mets.setPROFILE(getProfile()); // MetsHdr MetsHdr metsHdr = new MetsHdr(); metsHdr.setCREATEDATE(new Date()); // FIXME: CREATEDATE is now: // maybe should be item create // date? // Agent Agent agent = new Agent(); agent.setROLE(Role.CUSTODIAN); agent.setTYPE(Type.ORGANIZATION); Name name = new Name(); name.getContent() .add(new PCData(ConfigurationManager .getProperty("dspace.name"))); agent.getContent().add(name); metsHdr.getContent().add(agent); mets.getContent().add(metsHdr); // add DMD sections // Each type element MAY be either just a MODS-and-crosswalk name, OR // a combination "MODS-name:crosswalk-name" (e.g. "DC:qDC"). String dmdTypes[] = getDmdTypes(params); // record of ID of each dmdsec to make DMDID in structmap. String dmdGroup = gensym("dmd_group"); String dmdId[] = new String[dmdTypes.length]; for (int i = 0; i < dmdTypes.length; ++i) { dmdId[i] = gensym("dmd"); XmlData xmlData = new XmlData(); String xwalkName, metsName; String parts[] = dmdTypes[i].split(":", 2); if (parts.length > 1) { metsName = parts[0]; xwalkName = parts[1]; } else xwalkName = metsName = dmdTypes[i]; DisseminationCrosswalk xwalk = (DisseminationCrosswalk) PluginManager.getNamedPlugin(DisseminationCrosswalk.class, xwalkName); if (xwalk == null) throw new PackageValidationException("Cannot find "+dmdTypes[i]+" crosswalk plugin!"); else crosswalkToMets(xwalk, item, xmlData); DmdSec dmdSec = new DmdSec(); dmdSec.setID(dmdId[i]); dmdSec.setGROUPID(dmdGroup); MdWrap mdWrap = new MdWrap(); setMdType(mdWrap, metsName); mdWrap.getContent().add(xmlData); dmdSec.getContent().add(mdWrap); mets.getContent().add(dmdSec); } // Only add license AMD section if there are any licenses. // Catch authorization failures accessing license bitstreams // only if we are skipping unauthorized bitstreams. String licenseID = null; try { AmdSec amdSec = new AmdSec(); addRightsMd(context, item, amdSec); if (amdSec.getContent().size() > 0) { licenseID = gensym("license"); amdSec.setID(licenseID); mets.getContent().add(amdSec); } } catch (AuthorizeException e) { String unauth = (params == null) ? null : params.getProperty("unauthorized"); if (!(unauth != null && unauth.equalsIgnoreCase("skip"))) throw e; else log.warn("Skipping license metadata because of access failure: "+e.toString()); } // FIXME: History data???? Nooooo!!!! // fileSec - all non-metadata bundles go into fileGrp, // and each bitstream therein into a file. // Create the bitstream-level techMd and div's for structmap // at the same time so we can connec the IDREFs to IDs. FileSec fileSec = new FileSec(); String techMdType = getTechMdType(params); String parts[] = techMdType.split(":", 2); String xwalkName, metsName; if (parts.length > 1) { metsName = parts[0]; xwalkName = parts[1]; } else xwalkName = metsName = techMdType; DisseminationCrosswalk xwalk = (DisseminationCrosswalk) PluginManager.getNamedPlugin(DisseminationCrosswalk.class, xwalkName); if (xwalk == null) throw new PackageValidationException("Cannot find "+xwalkName+" crosswalk plugin!"); // log the primary bitstream for structmap String primaryBitstreamFileID = null; // accumulate content DIV items to put in structMap later. List contentDivs = new ArrayList(); // how to handle unauthorized bundle/bitstream: String unauth = (params == null) ? null : params.getProperty("unauthorized"); Bundle[] bundles = item.getBundles(); for (int i = 0; i < bundles.length; i++) { if (PackageUtils.isMetaInfoBundle(bundles[i])) continue; // unauthorized bundle? // NOTE: This must match the logic in disseminate() if (!AuthorizeManager.authorizeActionBoolean(context, bundles[i], Constants.READ)) { if (unauth != null && (unauth.equalsIgnoreCase("skip"))) continue; else throw new AuthorizeException("Not authorized to read Bundle named \""+bundles[i].getName()+"\""); } Bitstream[] bitstreams = bundles[i].getBitstreams(); // Create a fileGrp FileGrp fileGrp = new FileGrp(); // Bundle name for USE attribute String bName = bundles[i].getName(); if ((bName != null) && !bName.equals("")) fileGrp.setUSE(bundleToFileGrp(bName)); // watch for primary bitstream int primaryBitstreamID = -1; boolean isContentBundle = false; if ((bName != null) && bName.equals("ORIGINAL")) { isContentBundle = true; primaryBitstreamID = bundles[i].getPrimaryBitstreamID(); } for (int bits = 0; bits < bitstreams.length; bits++) { // Check for authorization. Handle unauthorized // bitstreams to match the logic in disseminate(), // i.e. "unauth=zero" means include a 0-length bitstream, // "unauth=skip" means to ignore it (and exclude from // manifest). boolean auth = AuthorizeManager.authorizeActionBoolean(context, bitstreams[bits], Constants.READ); if (!auth) { if (unauth != null && unauth.equalsIgnoreCase("skip")) continue; else if (!(unauth != null && unauth.equalsIgnoreCase("zero"))) throw new AuthorizeException("Not authorized to read Bitstream, SID="+String.valueOf(bitstreams[bits].getSequenceID())); } String sid = String.valueOf(bitstreams[bits].getSequenceID()); edu.harvard.hul.ois.mets.File file = new edu.harvard.hul.ois.mets.File(); String xmlIDstart = "bitstream_"; String fileID = xmlIDstart + sid; file.setID(fileID); // log primary bitstream for later (structMap) if (bitstreams[bits].getID() == primaryBitstreamID) primaryBitstreamFileID = fileID; // if this is content, add to structmap too: if (isContentBundle) { Div div = new Div(); div.setID(gensym("div")); div.setTYPE("DSpace Content Bitstream"); Fptr fptr = new Fptr(); fptr.setFILEID(fileID); div.getContent().add(fptr); contentDivs.add(div); } file.setSEQ(bitstreams[bits].getSequenceID()); String groupID = "GROUP_" + xmlIDstart + sid; /* * If we're in THUMBNAIL or TEXT bundles, the bitstream is * extracted text or a thumbnail, so we use the name to work * out which bitstream to be in the same group as */ if ((bundles[i].getName() != null) && (bundles[i].getName().equals("THUMBNAIL") || bundles[i].getName().startsWith("TEXT"))) { // Try and find the original bitstream, and chuck the // derived bitstream in the same group Bitstream original = findOriginalBitstream(item, bitstreams[bits]); if (original != null) { groupID = "GROUP_" + xmlIDstart + original.getSequenceID(); } } file.setGROUPID(groupID); file.setMIMETYPE(bitstreams[bits].getFormat().getMIMEType()); // FIXME: CREATED: no date file.setSIZE(auth ? bitstreams[bits].getSize() : 0); // translate checksum and type to METS, if available. String csType = bitstreams[bits].getChecksumAlgorithm(); String cs = bitstreams[bits].getChecksum(); if (auth && cs != null && csType != null) { try { file.setCHECKSUMTYPE(Checksumtype.parse(csType)); file.setCHECKSUM(cs); } catch (MetsException e) { log.warn("Cannot set bitstream checksum type="+csType+" in METS."); } } // FLocat: filename is MD5 checksum FLocat flocat = new FLocat(); flocat.setLOCTYPE(Loctype.URL); flocat.setXlinkHref(makeBitstreamName(bitstreams[bits])); // Make bitstream techMD metadata, add to file. String techID = "techMd_for_bitstream_"+bitstreams[bits].getSequenceID(); AmdSec fAmdSec = new AmdSec(); fAmdSec.setID(techID); TechMD techMd = new TechMD(); techMd.setID(gensym("tech")); MdWrap mdWrap = new MdWrap(); setMdType(mdWrap, metsName); XmlData xmlData = new XmlData(); mdWrap.getContent().add(xmlData); techMd.getContent().add(mdWrap); fAmdSec.getContent().add(techMd); mets.getContent().add(fAmdSec); crosswalkToMets(xwalk, bitstreams[bits], xmlData); file.setADMID(techID); // Add FLocat to File, and File to FileGrp file.getContent().add(flocat); fileGrp.getContent().add(file); } // Add fileGrp to fileSec fileSec.getContent().add(fileGrp); } // Add fileSec to document mets.getContent().add(fileSec); // Create simple structMap: initial div represents the Item, // and user-visible content bitstreams are in its child divs. StringBuffer dmdIds = new StringBuffer(); for (int i = 0; i < dmdId.length; ++i) dmdIds.append(" "+dmdId[i]); StructMap structMap = new StructMap(); structMap.setID(gensym("struct")); structMap.setTYPE("LOGICAL"); structMap.setLABEL("DSpace"); Div div0 = new Div(); div0.setID(gensym("div")); div0.setTYPE("DSpace Item"); div0.setDMDID(dmdIds.substring(1)); if (licenseID != null) div0.setADMID(licenseID); // if there is a primary bitstream, add FPTR to it. if (primaryBitstreamFileID != null) { Fptr fptr = new Fptr(); fptr.setFILEID(primaryBitstreamFileID); div0.getContent().add(fptr); } // add DIV for each content bitstream div0.getContent().addAll(contentDivs); structMap.getContent().add(div0); // Does subclass have something to add to structMap? addStructMap(context, item, params, mets); mets.getContent().add(structMap); mets.validate(new MetsValidator()); mets.write(new MetsWriter(out)); } catch (MetsException e) { // We don't pass up a MetsException, so callers don't need to // know the details of the METS toolkit // e.printStackTrace(); throw new PackageValidationException(e); } } /** * For a bitstream that's a thumbnail or extracted text, find the * corresponding bitstream it was derived from, in the ORIGINAL bundle. * * @param item * the item we're dealing with * @param derived * the derived bitstream * * @return the corresponding original bitstream (or null) */ protected static Bitstream findOriginalBitstream(Item item, Bitstream derived) throws SQLException { Bundle[] bundles = item.getBundles(); // Filename of original will be filename of the derived bitstream // minus the extension (last 4 chars - .jpg or .txt) String originalFilename = derived.getName().substring(0, derived.getName().length() - 4); // First find "original" bundle for (int i = 0; i < bundles.length; i++) { if ((bundles[i].getName() != null) && bundles[i].getName().equals("ORIGINAL")) { // Now find the corresponding bitstream Bitstream[] bitstreams = bundles[i].getBitstreams(); for (int bsnum = 0; bsnum < bitstreams.length; bsnum++) { if (bitstreams[bsnum].getName().equals(originalFilename)) { return bitstreams[bsnum]; } } } } // Didn't find it return null; } // Get result from crosswalk plugin and add it to the document, // including namespaces and schema. private void crosswalkToMets(DisseminationCrosswalk xwalk, DSpaceObject dso, MetsElement me) throws CrosswalkException, IOException, SQLException, AuthorizeException { // add crosswalk's namespaces and schemaLocation to this element: String raw = xwalk.getSchemaLocation(); String sloc[] = raw == null ? null : raw.split("\\s+"); Namespace ns[] = xwalk.getNamespaces(); for (int i = 0; i < ns.length; ++i) { String uri = ns[i].getURI(); if (sloc != null && sloc.length > 1 && uri.equals(sloc[0])) me.setSchema(ns[i].getPrefix(), uri, sloc[1]); else me.setSchema(ns[i].getPrefix(), uri); } // add result of crosswalk PreformedXML pXML = new PreformedXML( xwalk.preferList() ? outputter.outputString(xwalk.disseminateList(dso)) : outputter.outputString(xwalk.disseminateElement(dso))); me.getContent().add(pXML); } /** * Returns name of METS profile to which this package conforms, e.g. * "DSpace METS DIP Profile 1.0" * @return string name of profile. */ abstract public String getProfile(); /** * Returns fileGrp's USE attribute value corresponding to a DSpace bundle name. * * @param bname name of DSpace bundle. * @return string name of fileGrp */ abstract public String bundleToFileGrp(String bname); /** * Get the types of Item-wide DMD to include in package. * Each element of the returned array is a String, which * MAY be just a simple name, naming both the Crosswalk Plugin and * the METS "MDTYPE", <em>or</em> a colon-separated pair consisting of * the METS name followed by a colon and the Crosswalk Plugin name. * E.g. the type string <code>"DC:qualifiedDublinCore"</code> tells it to * create a METS section with <code>MDTYPE="DC"</code> and use the plugin * named "qualifiedDublinCore" to obtain the data. * @param params the PackageParameters passed to the disseminator. * @return array of metadata type strings, never null. */ abstract public String [] getDmdTypes(PackageParameters params) throws SQLException, IOException, AuthorizeException; /** * Get the type string of the technical metadata to create for each * Bitstream in the Item. The type string may be a simple name or * colon-separated compound as specified for <code>getDmdTypes()</code> above. * @param params the PackageParameters passed to the disseminator. * @return array of metadata type strings, never null. */ abstract public String getTechMdType(PackageParameters params) throws SQLException, IOException, AuthorizeException; /** * Add Rights metadata for the Item, in the form of * (<code>rightsMd</code> elements) to the given metadata section. * */ abstract public void addRightsMd(Context context, Item item, AmdSec amdSec) throws SQLException, IOException, AuthorizeException, MetsException; /** * Add any additional <code>structMap</code> elements to the * METS document, as required by this subclass. A simple default * structure map which fulfills the minimal DSpace METS DIP/SIP * requirements is already present, so this does not need to do anything. * @param mets the METS document to which to add structMaps */ abstract public void addStructMap(Context context, Item item, PackageParameters params, Mets mets) throws SQLException, IOException, AuthorizeException, MetsException; }