org.mycore.datamodel.ifs.MCRFile.java Source code

Java tutorial

Introduction

Here is the source code for org.mycore.datamodel.ifs.MCRFile.java

Source

/*
 * $Revision$ $Date$ This file is part of M y C o R e See http://www.mycore.de/ for details. This program
 * is free software; you can use it, redistribute it and / or modify it under the terms of the GNU General Public License (GPL) as published by the Free
 * Software Foundation; either version 2 of the License or (at your option) any later version. This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details. You should have received a copy of the GNU General Public License along with this program, in a file called gpl.txt or license.txt. If not,
 * write to the Free Software Foundation Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
 */

package org.mycore.datamodel.ifs;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.util.Collection;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.output.XMLOutputter;
import org.mycore.common.MCRPersistenceException;
import org.mycore.common.MCRUsageException;
import org.mycore.common.content.MCRContent;
import org.mycore.common.events.MCREvent;
import org.mycore.common.events.MCREventManager;
import org.mycore.datamodel.classifications2.MCRCategLinkReference;
import org.mycore.datamodel.classifications2.MCRCategLinkServiceFactory;
import org.mycore.datamodel.classifications2.MCRCategoryID;
import org.mycore.datamodel.common.MCRISO8601Date;
import org.mycore.datamodel.metadata.MCRMetadataManager;
import org.mycore.datamodel.metadata.MCRObject;
import org.mycore.datamodel.metadata.MCRObjectID;
import org.mycore.datamodel.niofs.MCRFileAttributes;
import org.mycore.datamodel.niofs.ifs1.MCRFileChannel;
import org.xml.sax.SAXException;

/**
 * Represents a stored file with its metadata and content.
 * 
 * @author Frank Ltzenkirchen
 * @version $Revision$ $Date$
 */
public class MCRFile extends MCRFilesystemNode implements MCRFileReader {

    private static Pattern MD5_HEX_PATTERN = Pattern.compile("[a-fA-F0-9]{32}");

    private static Set<? extends OpenOption> supportedOptions = EnumSet.of(StandardOpenOption.APPEND,
            StandardOpenOption.DSYNC, StandardOpenOption.READ, StandardOpenOption.SPARSE, StandardOpenOption.SYNC,
            StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);

    /** The ID of the store that holds this file's content */
    protected String storeID;

    /** The ID that identifies the place where the store holds the content */
    protected String storageID;

    /** The ID of the content type of this file */
    protected String contentTypeID;

    /** The md5 checksum that was built when content was read for this file */
    protected String md5;

    /** The optional extender for streaming audio/video files */
    protected MCRAudioVideoExtender avExtender;

    /** Is true if this file is a new MCRFile and not retrieved from store * */
    private boolean isNew;

    /**
     * Creates a new and empty root MCRFile with the given filename, belonging to the given ownerID. The file is assumed to be a standalone "root file" that has
     * no parent directory.
     * 
     * @param name
     *            the filename of the new MCRFile
     * @param ownerID
     *            any ID String of the logical owner of this file
     */
    public MCRFile(String name, String ownerID) {
        super(name, ownerID);
        initContentFields();
        setNew(true);
        storeNew();
    }

    /**
     * Creates a new, empty MCRFile with the given filename in the parent MCRDirectory.
     * 
     * @param name
     *            the filename of the new MCRFile
     * @param parent
     *            the parent directory that will contain the new child
     * @throws MCRUsageException
     *             if that directory already contains a child with that name
     */
    public MCRFile(String name, MCRDirectory parent) {
        this(name, parent, true);
    }

    /**
     * Creates a new, empty MCRFile with the given filename in the parent MCRDirectory.
     * 
     * @param name
     *            the filename of the new MCRFile
     * @param parent
     *            the parent directory that will contain the new child
     * @param doExistCheck
     *              checks if file with that Name already exists 
     * @throws MCRUsageException
     *             if that directory already contains a child with that name
     */
    public MCRFile(String name, MCRDirectory parent, boolean doExistCheck) {
        super(name, parent, doExistCheck);
        initContentFields();
        setNew(true);
        storeNew();
    }

    /*
     * Internal constructor, do not use on your own.
     */
    public MCRFile(String ID, String parentID, String ownerID, String name, String label, long size,
            GregorianCalendar date, String storeID, String storageID, String fctID, String md5) {
        super(ID, parentID, ownerID, name, label, size, date);

        this.storageID = storageID;
        this.storeID = storeID;
        contentTypeID = fctID;
        this.md5 = md5;
        setNew(false);
    }

    /**
     * Returns the MCRFile with the given ID.
     * 
     * @param ID
     *            the unique ID of the MCRFile to return
     * @return the MCRFile with the given ID, or null if no such file exists
     */
    public static MCRFile getFile(String ID) {
        return (MCRFile) MCRFilesystemNode.getNode(ID);
    }

    /**
     * Returns the root MCRFile that has no parent and is logically owned by the object with the given ID.
     * 
     * @param ownerID
     *            the ID of the logical owner of that file
     * @return the root MCRFile stored for that owner ID, or null if no such file exists
     */
    public static MCRFile getRootFile(String ownerID) {
        return (MCRFile) MCRFilesystemNode.getRootNode(ownerID);
    }

    /**
     * Sets initial values for the fields of a new, empty MCRFile
     */
    private void initContentFields() {
        storageID = "";
        contentTypeID = MCRFileContentTypeFactory.detectType(name, null).getID();
        md5 = "d41d8cd98f00b204e9800998ecf8427e"; // md5 of empty file
        size = 0;
        avExtender = null;
        MCRContentStore store = MCRContentStoreFactory.selectStore(this);
        storeID = store.getID();
    }

    /**
     * Returns the file extension of this file's name
     * 
     * @return the file extension, or an empty string if the file has no extension
     */
    public String getExtension() {
        ensureNotDeleted();

        if (name.endsWith(".")) {
            return "";
        }

        int pos = name.lastIndexOf(".");

        return pos == -1 ? "" : name.substring(pos + 1);
    }

    /**
     * Returns the MD5 checksum for this file
     */
    public String getMD5() {
        ensureNotDeleted();

        return md5;
    }

    /**
     * Returns the ID of the MCRContentStore implementation that holds the content of this file
     */
    public String getStoreID() {
        ensureNotDeleted();

        return storeID;
    }

    /**
     * Returns the storage ID that identifies the place where the MCRContentStore has stored the content of this file
     */
    public String getStorageID() {
        ensureNotDeleted();

        return storageID;
    }

    /**
     * Returns the local java.io.File representing this stored file. Be careful
     * to use this only for reading data, do never modify directly!
     * 
     * @return the file in the local filesystem representing this file
     */
    public File getLocalFile() throws IOException {
        MCRContentStore contentStore = getContentStore();
        if (contentStore == null) {
            throw new IOException("Cannot read file as its content store is unknown:" + this.toString());
        }
        return contentStore.getLocalFile(this);
    }

    /**
     * Opens a file, returning a seekable byte channel to access the file.
     * See 
     * @throws  IllegalArgumentException
     *          if the set contains an invalid combination of options
     * @throws  UnsupportedOperationException
     *          if an unsupported open option is specified
     * @throws  IOException
     *          if an I/O error occurs
     *
     * @see java.nio.channels.FileChannel#open(Path,Set,FileAttribute[])
     */
    public FileChannel getFileChannel(Set<? extends OpenOption> options) throws IOException {
        for (OpenOption option : options) {
            checkOpenOption(option);
        }
        File localFile;
        try {
            localFile = getLocalFile();
        } catch (IOException e) {
            throw new NoSuchFileException(toPath().toString(), null, e.getMessage());
        }
        FileChannel fileChannel = FileChannel.open(localFile.toPath(), options);
        boolean write = options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.APPEND);
        boolean read = options.contains(StandardOpenOption.READ) || !write;
        return new MCRFileChannel(this, (FileChannel) fileChannel, write);
    }

    /**
     * checks if supplied {@link OpenOption} is supported by {@link #getFileChannel(Set)}.
     */
    public static void checkOpenOption(OpenOption option) {
        if (!supportedOptions.contains(option)) {
            throw new UnsupportedOperationException(
                    "Unsupported OpenOption: " + option.getClass().getSimpleName() + "." + option);
        }
    }

    /**
     * Returns the MCRContentStore instance that holds the content of this file
     * 
     * @return the MCRContentStore instance that holds the content of this file, or null if no content is stored
     */
    protected MCRContentStore getContentStore() {
        if (storeID.length() == 0) {
            return null;
        }
        return MCRContentStoreFactory.getStore(storeID);
    }

    /**
     * Reads the content of this file from a java.lang.String and stores its text as bytes, encoded in the default encoding of the platform where this is
     * running.
     * 
     * @param source
     *            the String that is the file's content
     */
    public void setContentFrom(String source) throws MCRPersistenceException {
        Objects.requireNonNull(source, "source string is null");

        byte[] bytes = source.getBytes(Charset.defaultCharset());

        setContentFrom(bytes);
    }

    /**
     * Reads the content of this file from a java.lang.String and stores its text as bytes, encoded in the encoding given, in an MCRContentStore.
     * 
     * @param source
     *            the String that is the file's content
     * @param encoding
     *            the character encoding to use to store the String as bytes
     */
    public void setContentFrom(String source, String encoding)
            throws MCRPersistenceException, UnsupportedEncodingException {
        Objects.requireNonNull(source, "source string is null");
        Objects.requireNonNull(source, "source string encoding is null");

        byte[] bytes = source.getBytes(encoding);

        setContentFrom(bytes);
    }

    /**
     * Reads the content of this file from a source file in the local filesystem and stores it in an MCRContentStore.
     * 
     * @param source
     *            the file in the local host's filesystem thats content should be imported
     */
    public void setContentFrom(File source) throws MCRPersistenceException {
        Objects.requireNonNull(source, "source file is null");
        if (!source.exists()) {
            throw new MCRUsageException("source file does not exist:" + source.getPath());
        }
        if (!source.canRead()) {
            throw new MCRUsageException("source file not readable:" + source.getPath());
        }
        FileInputStream fin = null;

        try {
            fin = new FileInputStream(source);
        } catch (FileNotFoundException ignored) {
        } // We already checked it exists

        setContentFrom(new BufferedInputStream(fin));
    }

    /**
     * Reads the content of this file from a byte array and stores it in an MCRContentStore.
     * 
     * @param source
     *            the file's content
     */
    public void setContentFrom(byte[] source) throws MCRPersistenceException {
        Objects.requireNonNull(source, "source byte array is null");

        setContentFrom(new ByteArrayInputStream(source));
    }

    /**
     * Sets the content of this file from a JDOM xml document.
     * 
     * @param xml
     *            the JDOM xml document that should be stored as file content
     */
    public void setContentFrom(Document xml) {
        Objects.requireNonNull(xml, "jdom xml document is null");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            new XMLOutputter().output(xml, baos);
            baos.close();
        } catch (IOException ignored) {
        }
        setContentFrom(baos.toByteArray());
    }

    /**
     * Reads the content of this file from the source InputStream and stores it in an MCRContentStore. InputStream does NOT get closed at end of process, this
     * must be done by invoking code if required/appropriate.
     * 
     * @param source
     *            the source for the file's content bytes
     */
    public void setContentFrom(InputStream source) throws MCRPersistenceException {
        setContentFrom(source, true);
    }

    public long setContentFrom(InputStream source, boolean storeContentChange) throws MCRPersistenceException {
        ensureNotDeleted();

        String old_md5 = md5;
        long old_size = size;
        String old_storageID = storageID;
        String old_storeID = storeID;
        MCRContentStore old_store = getContentStore();

        initContentFields();

        MCRContentInputStream cis = new MCRContentInputStream(source);
        byte[] header = cis.getHeader();

        contentTypeID = MCRFileContentTypeFactory.detectType(getName(), header).getID();

        MCRContentStore store = MCRContentStoreFactory.selectStore(this);

        storageID = store.storeContent(this, cis);
        storeID = store.getID();

        long new_size = cis.getLength();
        String new_md5 = cis.getMD5String();

        if ((old_storageID.length() != 0)
                && (!(old_storageID.equals(storageID) && (old_storeID.equals(storeID))))) {
            old_store.deleteContent(old_storageID);
        }

        boolean changed = new_size != old_size || new_md5.equals(old_md5);

        if (changed) {
            if (storeContentChange) {
                adjustMetadata(FileTime.fromMillis(System.currentTimeMillis()), new_md5, new_size);
            } else {
                this.md5 = new_md5;
            }
        }

        return new_size - old_size;
    }

    public void storeContentChange(long sizeDiff) {
        adjustMetadata(FileTime.fromMillis(System.currentTimeMillis()), md5, size + sizeDiff);
    }

    public synchronized void adjustMetadata(FileTime lastModified, String md5, long newSize) {
        long sizeDiff = newSize - getSize();
        if (sizeDiff == 0 && this.md5.equals(md5) && lastModified == null) {
            return;
        }
        if (!MD5_HEX_PATTERN.matcher(md5).matches()) {
            throw new IllegalArgumentException("MD5 sum is not valid: " + md5);
        }
        this.md5 = md5;
        this.size = newSize;
        touch(lastModified, false);
        if (hasParent()) {
            getParent().sizeOfChildChanged(sizeDiff);
        }

        // If file content has changed, call event handlers to index content
        String type = isNew() ? MCREvent.CREATE_EVENT : MCREvent.UPDATE_EVENT;
        MCREvent event = new MCREvent(MCREvent.PATH_TYPE, type);
        event.put(MCRFileEventHandlerBase.FILE_TYPE, this); //to support old events
        event.put(MCREvent.PATH_KEY, toPath());
        event.put(MCREvent.FILEATTR_KEY, getBasicFileAttributes());
        MCREventManager.instance().handleEvent(event);
        setNew(false);
    }

    /**
     * Deletes this file and its content stored in the system. Note that after
     * calling this method, the file object is deleted and invalid and can not
     * be used any more.
     */
    @Override
    public void delete() throws MCRPersistenceException {
        ensureNotDeleted();

        if (storageID.length() != 0) {
            BasicFileAttributes attrs = getBasicFileAttributes();
            getContentStore().deleteContent(storageID);

            // Call event handlers to update indexed content
            MCREvent event = new MCREvent(MCREvent.PATH_TYPE, MCREvent.DELETE_EVENT);
            event.put(MCRFileEventHandlerBase.FILE_TYPE, this); //to support old events
            event.put(MCREvent.PATH_KEY, toPath());
            event.put(MCREvent.FILEATTR_KEY, attrs);
            MCREventManager.instance().handleEvent(event);

            if (hasParent()) {
                getParent().sizeOfChildChanged(-size);
            }
        }

        super.delete();

        contentTypeID = null;
        md5 = null;
        storageID = null;
        storeID = null;
        avExtender = null;
    }

    /**
     * Returns the content of this file as MCRContent instance.
     */
    public MCRContent getContent() throws IOException {
        return getContentStore().doRetrieveMCRContent(this);
    }

    /**
     * Gets an InputStream to read the content of this file from the underlying store. It is important that you close() the stream when you are finished reading
     * content from it.
     * 
     * @return an InputStream to read the file's content from
     */
    public InputStream getContentAsInputStream() throws IOException {
        return getContent().getInputStream();
    }

    /**
     * Writes the content of this file to a target output stream.
     * 
     * @param target
     *            the output stream to write the content to
     */
    public void getContentTo(OutputStream target) throws MCRPersistenceException {
        ensureNotDeleted();

        if (storageID.length() != 0) {
            try {
                getContent().sendTo(target);
            } catch (IOException e) {
                throw new MCRPersistenceException("Could not write content to target stream.", e);
            }
        }
    }

    /**
     * Writes the content of this file to a file on the local filesystem
     * 
     * @param target
     *            the local file to write the content to
     */
    public void getContentTo(File target) throws MCRPersistenceException, IOException {
        getContent().sendTo(target);
    }

    /**
     * Gets the content of this file as a byte array
     * 
     * @return the content of this file as a byte array
     */
    public byte[] getContentAsByteArray() throws MCRPersistenceException {
        try {
            return getContent().asByteArray();
        } catch (IOException e) {
            throw new MCRPersistenceException("Error while getting file content.", e);
        }
    }

    /**
     * Gets the content of this file as a string, using the default encoding of the system environment
     * 
     * @return the file's content as a String
     */
    public String getContentAsString() throws MCRPersistenceException {
        return new String(getContentAsByteArray(), Charset.defaultCharset());
    }

    /**
     * Gets the content of this file as a string, using the given encoding
     * 
     * @param encoding
     *            the character encoding to use
     * @return the file's content as a String
     */
    public String getContentAsString(String encoding) throws MCRPersistenceException, UnsupportedEncodingException {
        return new String(getContentAsByteArray(), encoding);
    }

    public org.jdom2.Document getContentAsJDOM()
            throws MCRPersistenceException, IOException, org.jdom2.JDOMException {
        try {
            return getContent().asXML();
        } catch (SAXException e) {
            throw new JDOMException("Could not parse XML file.", e);
        }
    }

    /**
     * Returns true, if this file is stored in a content store that provides an MCRAudioVideoExtender for audio/video streaming and additional metadata
     */
    public boolean hasAudioVideoExtender() {
        ensureNotDeleted();

        if (storeID.length() == 0) {
            return false;
        }
        return MCRContentStoreFactory.providesAudioVideoExtender(storeID);
    }

    /**
     * Returns the AudioVideoExtender in case this file is streaming audio/video and stored in a ContentStore that supports this
     */
    public MCRAudioVideoExtender getAudioVideoExtender() {
        ensureNotDeleted();

        if (hasAudioVideoExtender() && avExtender == null) {
            avExtender = MCRContentStoreFactory.buildExtender(this);
        }

        return avExtender;
    }

    /**
     * Gets the ID of the content type of this file
     */
    public String getContentTypeID() {
        ensureNotDeleted();

        return contentTypeID;
    }

    /**
     * Gets the content type of this file
     */
    public MCRFileContentType getContentType() {
        ensureNotDeleted();

        return MCRFileContentTypeFactory.getType(contentTypeID);
    }

    /**
     * checks if the file still exists in the underlying content store and the md5 sum still matches. 
     * @return true if it passes md5 sum check, else false
     * @throws IOException if file exist but is not readable.
     */
    public boolean isValid() throws IOException {
        return getContentStore().isValid(this);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString());
        sb.append("ContentType = ").append(contentTypeID).append(" ");
        sb.append("MD5         = ").append(md5).append(" ");
        sb.append("StoreID     = ").append(storeID).append(" ");
        sb.append("StorageID   = ").append(storageID);

        return sb.toString();
    }

    /**
     * Build a XML representation of all technical metadata of this MCRFile and its MCRAudioVideoExtender, if present. That xml can be used for indexing this
     * data.
     */
    public Document createXML() {
        Element root = new Element("file");
        root.setAttribute("id", getID());
        root.setAttribute("owner", getOwnerID());
        root.setAttribute("name", getName());
        String absolutePath = getAbsolutePath();
        root.setAttribute("path", absolutePath);
        root.setAttribute("size", Long.toString(getSize()));
        root.setAttribute("extension", getExtension());
        root.setAttribute("contentTypeID", getContentTypeID());
        root.setAttribute("contentType", getContentType().getLabel());
        root.setAttribute("returnId", getMCRObjectID().toString());
        Collection<MCRCategoryID> linksFromReference = MCRCategLinkServiceFactory.getInstance()
                .getLinksFromReference(getCategLinkReference(MCRObjectID.getInstance(getOwnerID()), absolutePath));
        for (MCRCategoryID category : linksFromReference) {
            Element catEl = new Element("category");
            catEl.setAttribute("id", category.toString());
            root.addContent(catEl);
        }

        MCRISO8601Date iDate = new MCRISO8601Date();
        iDate.setDate(getLastModified().getTime());
        root.setAttribute("modified", iDate.getISOString());

        if (hasAudioVideoExtender()) {
            MCRAudioVideoExtender ext = getAudioVideoExtender();
            root.setAttribute("bitRate", String.valueOf(ext.getBitRate()));
            root.setAttribute("frameRate", String.valueOf(ext.getFrameRate()));
            root.setAttribute("duration", ext.getDurationTimecode());
            root.setAttribute("mediaType", (ext.hasVideoStream() ? "video" : "audio"));
        }

        return new Document(root);
    }

    /**
     * Gets the {@link MCRObjectID} for the {@link MCRObject} where this file is related to (the owner id of the derivate).
     * 
     * @return the {@link MCRObjectID}
     */
    public MCRObjectID getMCRObjectID() {
        return MCRMetadataManager.getObjectId(MCRObjectID.getInstance(getOwnerID()), 10, TimeUnit.SECONDS);
    }

    /**
     * Returns a handle to a {@link MCRFile} given by the derivate id and the full path to the file. 
     * 
     * @param derivateID the id of the derivate containing the file
     * @param path the path to the file
     * 
     * @return a {@link MCRFile} or null if there is no such file under the given path within the derivate 
     */
    public static MCRFile getMCRFile(MCRObjectID derivateID, String path) {
        MCRFilesystemNode node = MCRFilesystemNode.getRootNode(derivateID.toString());
        if (!(node instanceof MCRDirectory)) {
            return null;
        }
        try {
            MCRFile f = (MCRFile) ((MCRDirectory) node).getChildByPath(path);
            return f;
        } catch (Exception ex) {
            return null;
        }
    }

    public static MCRCategLinkReference getCategLinkReference(MCRObjectID derivateID, String path) {
        MCRCategLinkReference ref = new MCRCategLinkReference(path, derivateID.toString());
        return ref;
    }

    public boolean isNew() {
        return isNew;
    }

    private void setNew(boolean isNew) {
        this.isNew = isNew;
    }

    @Override
    protected BasicFileAttributes getBasicFileAttributes() {
        return MCRFileAttributes.file(getID(), getSize(), getMD5(),
                FileTime.fromMillis(getLastModified().getTimeInMillis()));
    }

}