/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.core.persistence.xml;

import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.fs.BasedFileSystem;
import org.apache.jackrabbit.core.fs.FileSystem;
import org.apache.jackrabbit.core.fs.FileSystemException;
import org.apache.jackrabbit.core.fs.FileSystemResource;
import org.apache.jackrabbit.core.fs.local.LocalFileSystem;
import org.apache.jackrabbit.core.persistence.AbstractPersistenceManager;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NoSuchItemStateException;
import org.apache.jackrabbit.core.state.NodeReferences;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.persistence.PMContext;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.persistence.util.BLOBStore;
import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore;
import org.apache.jackrabbit.core.persistence.util.ResourceBasedBLOBStore;
import org.apache.jackrabbit.core.util.DOMWalker;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.NameFactory;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.PropertyType;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
 * <code>XMLPersistenceManager</code> is a <code>FileSystem</code>-based
 * <code>PersistenceManager</code> that persists <code>ItemState</code>
 * and <code>NodeReferences</code> objects in XML format.
 *
 * @deprecated Please migrate to a bundle persistence manager
 *   (<a href="https://issues.apache.org/jira/browse/JCR-2802">JCR-2802</a>)
 */
@Deprecated
public class XMLPersistenceManager extends AbstractPersistenceManager {

    private static Logger log = LoggerFactory.getLogger(XMLPersistenceManager.class);

    /**
     * hexdigits for toString
     */
    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

    /**
     * The default encoding used in serialization
     */
    public static final String DEFAULT_ENCODING = "UTF-8";

    /**
     * The XML elements and attributes used in serialization
     */
    private static final String NODE_ELEMENT = "node";
    private static final String UUID_ATTRIBUTE = "uuid";
    private static final String NODETYPE_ATTRIBUTE = "nodeType";
    private static final String PARENTUUID_ATTRIBUTE = "parentUUID";
    private static final String MODCOUNT_ATTRIBUTE = "modCount";

    private static final String MIXINTYPES_ELEMENT = "mixinTypes";
    private static final String MIXINTYPE_ELEMENT = "mixinType";

    private static final String PROPERTIES_ELEMENT = "properties";
    private static final String PROPERTY_ELEMENT = "property";
    private static final String NAME_ATTRIBUTE = "name";
    private static final String TYPE_ATTRIBUTE = "type";
    private static final String MULTIVALUED_ATTRIBUTE = "multiValued";

    private static final String VALUES_ELEMENT = "values";
    private static final String VALUE_ELEMENT = "value";

    private static final String NODES_ELEMENT = "nodes";

    private static final String NODEREFERENCES_ELEMENT = "references";
    private static final String TARGETID_ATTRIBUTE = "targetId";
    private static final String NODEREFERENCE_ELEMENT = "reference";
    private static final String PROPERTYID_ATTRIBUTE = "propertyId";

    private static final String NODEFILENAME = ".node.xml";

    private static final String NODEREFSFILENAME = ".references.xml";

    private boolean initialized;

    // file system where the item state is stored
    private FileSystem itemStateFS;
    // file system where BLOB data is stored
    private FileSystem blobFS;
    // BLOBStore that manages BLOB data in the file system
    private BLOBStore blobStore;

    /**
     * Template for the subdirectory path for the files associated with
     * a single node. The template is processed by replacing each
     * "<code>x</code>" with the next hex digit in the UUID string.
     * All other characters in the template are used as-is.
     */
    private String nodePathTemplate = "xxxx/xxxx/xxxxxxxxxxxxxxxxxxxxxxxx";

    private final NameFactory factory;

    /**
     * Creates a new <code>XMLPersistenceManager</code> instance.
     */
    public XMLPersistenceManager() {
        initialized = false;
        factory = NameFactoryImpl.getInstance();
    }

    /**
     * Returns the node path template.
     *
     * @return node path template
     */
    public String getNodePathTemplate() {
        return nodePathTemplate;
    }

    /**
     * Sets the node path template.
     *
     * @param template node path template
     */
    public void setNodePathTemplate(String template) {
        nodePathTemplate = template;
    }

    /**
     * Builds the path of the node folder for the given node identifier
     * based on the configured node path template.
     *
     * @param id node identifier
     * @return node folder path
     */
    private String buildNodeFolderPath(NodeId id) {
        StringBuilder sb = new StringBuilder();
        char[] chars = id.toString().toCharArray();
        int cnt = 0;
        for (int i = 0; i < nodePathTemplate.length(); i++) {
            char ch = nodePathTemplate.charAt(i);
            if (ch == 'x' && cnt < chars.length) {
                ch = chars[cnt++];
                if (ch == '-') {
                    ch = chars[cnt++];
                }
            }
            sb.append(ch);
        }
        return sb.toString();
    }

    private String buildPropFilePath(PropertyId id) {
        String fileName;
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(id.getName().getNamespaceURI().getBytes());
            md5.update(id.getName().getLocalName().getBytes());
            byte[] bytes = md5.digest();
            char[] chars = new char[32];
            for (int i = 0, j = 0; i < 16; i++) {
                chars[j++] = HEXDIGITS[(bytes[i] >> 4) & 0x0f];
                chars[j++] = HEXDIGITS[bytes[i] & 0x0f];
            }
            fileName = new String(chars) + ".xml";
        } catch (NoSuchAlgorithmException nsae) {
            // should never get here as MD5 should always be available in the JRE
            String msg = "MD5 not available";
            log.error(msg, nsae);
            throw new InternalError(msg + nsae);
        }
        return buildNodeFolderPath(id.getParentId()) + "/" + fileName;
    }

    private String buildNodeFilePath(NodeId id) {
        return buildNodeFolderPath(id) + "/" + NODEFILENAME;
    }

    private String buildNodeReferencesFilePath(NodeId id) {
        return buildNodeFolderPath(id) + "/" + NODEREFSFILENAME;
    }

    private void readState(DOMWalker walker, NodeState state)
            throws ItemStateException {
        // first do some paranoid sanity checks
        if (!walker.getName().equals(NODE_ELEMENT)) {
            String msg = "invalid serialization format (unexpected element: "
                    + walker.getName() + ")";
            log.debug(msg);
            throw new ItemStateException(msg);
        }
        // check uuid
        if (!state.getNodeId().toString().equals(walker.getAttribute(UUID_ATTRIBUTE))) {
            String msg = "invalid serialized state: uuid mismatch";
            log.debug(msg);
            throw new ItemStateException(msg);
        }
        // check nodetype
        String ntName = walker.getAttribute(NODETYPE_ATTRIBUTE);
        if (!factory.create(ntName).equals(state.getNodeTypeName())) {
            String msg = "invalid serialized state: nodetype mismatch";
            log.debug(msg);
            throw new ItemStateException(msg);
        }

        // now we're ready to read state

        // primary parent
        String parentUUID = walker.getAttribute(PARENTUUID_ATTRIBUTE);
        if (parentUUID.length() > 0) {
            state.setParentId(NodeId.valueOf(parentUUID));
        }

        // modification count
        String modCount = walker.getAttribute(MODCOUNT_ATTRIBUTE);
        state.setModCount(Short.parseShort(modCount));

        // mixin types
        if (walker.enterElement(MIXINTYPES_ELEMENT)) {
            Set<Name> mixins = new HashSet<Name>();
            while (walker.iterateElements(MIXINTYPE_ELEMENT)) {
                mixins.add(factory.create(walker.getAttribute(NAME_ATTRIBUTE)));
            }
            if (mixins.size() > 0) {
                state.setMixinTypeNames(mixins);
            }
            walker.leaveElement();
        }

        // property entries
        if (walker.enterElement(PROPERTIES_ELEMENT)) {
            while (walker.iterateElements(PROPERTY_ELEMENT)) {
                String propName = walker.getAttribute(NAME_ATTRIBUTE);
                // @todo deserialize type and values
                state.addPropertyName(factory.create(propName));
            }
            walker.leaveElement();
        }

        // child node entries
        if (walker.enterElement(NODES_ELEMENT)) {
            while (walker.iterateElements(NODE_ELEMENT)) {
                String childName = walker.getAttribute(NAME_ATTRIBUTE);
                String childUUID = walker.getAttribute(UUID_ATTRIBUTE);
                state.addChildNodeEntry(factory.create(childName), NodeId.valueOf(childUUID));
            }
            walker.leaveElement();
        }
    }

    private void readState(DOMWalker walker, PropertyState state)
            throws ItemStateException {
        // first do some paranoid sanity checks
        if (!walker.getName().equals(PROPERTY_ELEMENT)) {
            String msg = "invalid serialization format (unexpected element: "
                    + walker.getName() + ")";
            log.debug(msg);
            throw new ItemStateException(msg);
        }
        // check name
        if (!state.getName().equals(factory.create(walker.getAttribute(NAME_ATTRIBUTE)))) {
            String msg = "invalid serialized state: name mismatch";
            log.debug(msg);
            throw new ItemStateException(msg);
        }
        // check parentUUID
        NodeId parentId = NodeId.valueOf(walker.getAttribute(PARENTUUID_ATTRIBUTE));
        if (!parentId.equals(state.getParentId())) {
            String msg = "invalid serialized state: parentUUID mismatch";
            log.debug(msg);
            throw new ItemStateException(msg);
        }

        // now we're ready to read state

        // type
        String typeName = walker.getAttribute(TYPE_ATTRIBUTE);
        int type;
        try {
            type = PropertyType.valueFromName(typeName);
        } catch (IllegalArgumentException iae) {
            // should never be getting here
            throw new ItemStateException("unexpected property-type: " + typeName, iae);
        }
        state.setType(type);

        // multiValued
        String multiValued = walker.getAttribute(MULTIVALUED_ATTRIBUTE);
        state.setMultiValued(Boolean.parseBoolean(multiValued));

        // modification count
        String modCount = walker.getAttribute(MODCOUNT_ATTRIBUTE);
        state.setModCount(Short.parseShort(modCount));

        // values
        ArrayList<InternalValue> values = new ArrayList<InternalValue>();
        if (walker.enterElement(VALUES_ELEMENT)) {
            while (walker.iterateElements(VALUE_ELEMENT)) {
                // read serialized value
                String content = walker.getContent();
                if (PropertyType.STRING == type) {
                    // STRING value can be empty; ignore length
                    values.add(InternalValue.valueOf(content, type));
                } else if (content.length() > 0) {
                    // non-empty non-STRING value
                    if (type == PropertyType.BINARY) {
                        try {
                            // special handling required for binary value:
                            // the value stores the id of the BLOB data
                            // in the BLOB store
                            if (blobStore instanceof ResourceBasedBLOBStore) {
                                // optimization: if the BLOB store is resource-based
                                // retrieve the resource directly rather than having
                                // to read the BLOB from an input stream
                                FileSystemResource fsRes =
                                        ((ResourceBasedBLOBStore) blobStore).getResource(content);
                                values.add(InternalValue.create(fsRes));
                            } else {
                                InputStream in = blobStore.get(content);
                                try {
                                    values.add(InternalValue.create(in));
                                } finally {
                                    IOUtils.closeQuietly(in);
                                }
                            }
                        } catch (Exception e) {
                            String msg = "error while reading serialized binary value";
                            log.debug(msg);
                            throw new ItemStateException(msg, e);
                        }
                    } else {
                        // non-empty non-STRING non-BINARY value
                        values.add(InternalValue.valueOf(content, type));
                    }
                } else {
                    // empty non-STRING value
                    log.warn(state.getPropertyId() + ": ignoring empty value of type "
                            + PropertyType.nameFromValue(type));
                }
            }
            walker.leaveElement();
        }
        state.setValues((InternalValue[])
                values.toArray(new InternalValue[values.size()]));
    }

    private void readState(DOMWalker walker, NodeReferences refs)
            throws ItemStateException {
        // first do some paranoid sanity checks
        if (!walker.getName().equals(NODEREFERENCES_ELEMENT)) {
            String msg = "invalid serialization format (unexpected element: " + walker.getName() + ")";
            log.debug(msg);
            throw new ItemStateException(msg);
        }
        // check targetId
        if (!refs.getTargetId().equals(NodeId.valueOf(walker.getAttribute(TARGETID_ATTRIBUTE)))) {
            String msg = "invalid serialized state: targetId  mismatch";
            log.debug(msg);
            throw new ItemStateException(msg);
        }

        // now we're ready to read the references data

        // property id's
        refs.clearAllReferences();
        while (walker.iterateElements(NODEREFERENCE_ELEMENT)) {
            refs.addReference(PropertyId.valueOf(walker.getAttribute(PROPERTYID_ATTRIBUTE)));
        }
    }

    //---------------------------------------------------< PersistenceManager >
    /**
     * {@inheritDoc}
     */
    public void init(PMContext context) throws Exception {
        if (initialized) {
            throw new IllegalStateException("already initialized");
        }

        itemStateFS = new BasedFileSystem(context.getFileSystem(), "/data");

        /**
         * store BLOB data in local file system in a sub directory
         * of the workspace home directory
         */
        LocalFileSystem blobFS = new LocalFileSystem();
        blobFS.setRoot(new File(context.getHomeDir(), "blobs"));
        blobFS.init();
        this.blobFS = blobFS;
        blobStore = new FileSystemBLOBStore(blobFS);

        initialized = true;
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void close() throws Exception {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        try {
            // close BLOB file system
            blobFS.close();
            blobFS = null;
            blobStore = null;
            /**
             * there's no need close the item state store because it
             * is based in the workspace's file system which is
             * closed by the repository
             */
        } finally {
            initialized = false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized NodeState load(NodeId id)
            throws NoSuchItemStateException, ItemStateException {

        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        Exception e = null;
        String nodeFilePath = buildNodeFilePath(id);

        try {
            if (!itemStateFS.isFile(nodeFilePath)) {
                throw new NoSuchItemStateException(id.toString());
            }
            InputStream in = itemStateFS.getInputStream(nodeFilePath);

            try {
                DOMWalker walker = new DOMWalker(in);
                String ntName = walker.getAttribute(NODETYPE_ATTRIBUTE);

                NodeState state = createNew(id);
                state.setNodeTypeName(factory.create(ntName));
                readState(walker, state);
                return state;
            } finally {
                in.close();
            }
        } catch (IOException ioe) {
            e = ioe;
            // fall through
        } catch (FileSystemException fse) {
            e = fse;
            // fall through
        }
        String msg = "failed to read node state: " + id;
        log.debug(msg);
        throw new ItemStateException(msg, e);
    }

    /**
     * {@inheritDoc}
     */
    public synchronized PropertyState load(PropertyId id)
            throws NoSuchItemStateException, ItemStateException {

        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        Exception e = null;
        String propFilePath = buildPropFilePath(id);

        try {
            if (!itemStateFS.isFile(propFilePath)) {
                throw new NoSuchItemStateException(id.toString());
            }
            InputStream in = itemStateFS.getInputStream(propFilePath);
            try {
                DOMWalker walker = new DOMWalker(in);
                PropertyState state = createNew(id);
                readState(walker, state);
                return state;
            } finally {
                in.close();
            }
        } catch (IOException ioe) {
            e = ioe;
            // fall through
        } catch (FileSystemException fse) {
            e = fse;
            // fall through
        }
        String msg = "failed to read property state: " + id.toString();
        log.debug(msg);
        throw new ItemStateException(msg, e);
    }

    /**
     * {@inheritDoc}
     */
    protected void store(NodeState state) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        NodeId id = state.getNodeId();
        String nodeFilePath = buildNodeFilePath(id);
        FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath);
        try {
            nodeFile.makeParentDirs();
            OutputStream os = nodeFile.getOutputStream();
            Writer writer = null;
            try {
                String encoding = DEFAULT_ENCODING;
                try {
                    writer = new BufferedWriter(new OutputStreamWriter(os, encoding));
                } catch (UnsupportedEncodingException e) {
                    // should never get here!
                    OutputStreamWriter osw = new OutputStreamWriter(os);
                    encoding = osw.getEncoding();
                    writer = new BufferedWriter(osw);
                }

                String parentId = (state.getParentId() == null) ? "" : state.getParentId().toString();
                String encodedNodeType = Text.encodeIllegalXMLCharacters(state.getNodeTypeName().toString());
                writer.write("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>\n");
                writer.write("<" + NODE_ELEMENT + " "
                        + UUID_ATTRIBUTE + "=\"" + id + "\" "
                        + PARENTUUID_ATTRIBUTE + "=\"" + parentId + "\" "
                        + MODCOUNT_ATTRIBUTE + "=\"" + state.getModCount() + "\" "
                        + NODETYPE_ATTRIBUTE + "=\"" + encodedNodeType + "\">\n");

                // mixin types
                writer.write("\t<" + MIXINTYPES_ELEMENT + ">\n");
                for (Name mixin : state.getMixinTypeNames()) {
                    writer.write("\t\t<" + MIXINTYPE_ELEMENT + " "
                            + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(mixin.toString()) + "\"/>\n");
                }
                writer.write("\t</" + MIXINTYPES_ELEMENT + ">\n");

                // properties
                writer.write("\t<" + PROPERTIES_ELEMENT + ">\n");
                for (Name propName : state.getPropertyNames()) {
                    writer.write("\t\t<" + PROPERTY_ELEMENT + " "
                            + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(propName.toString()) + "\">\n");
                    // @todo serialize type, definition id and values
                    writer.write("\t\t</" + PROPERTY_ELEMENT + ">\n");
                }
                writer.write("\t</" + PROPERTIES_ELEMENT + ">\n");

                // child nodes
                writer.write("\t<" + NODES_ELEMENT + ">\n");
                for (ChildNodeEntry entry : state.getChildNodeEntries()) {
                    writer.write("\t\t<" + NODE_ELEMENT + " "
                            + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(entry.getName().toString()) + "\" "
                            + UUID_ATTRIBUTE + "=\"" + entry.getId() + "\">\n");
                    writer.write("\t\t</" + NODE_ELEMENT + ">\n");
                }
                writer.write("\t</" + NODES_ELEMENT + ">\n");

                writer.write("</" + NODE_ELEMENT + ">\n");
            } finally {
                writer.close();
            }
        } catch (Exception e) {
            String msg = "failed to write node state: " + id;
            log.debug(msg);
            throw new ItemStateException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void store(PropertyState state) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        String propFilePath = buildPropFilePath(state.getPropertyId());
        FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath);
        try {
            propFile.makeParentDirs();
            OutputStream os = propFile.getOutputStream();
            // write property state to xml file
            Writer writer = null;
            try {
                String encoding = DEFAULT_ENCODING;
                try {
                    writer = new BufferedWriter(new OutputStreamWriter(os, encoding));
                } catch (UnsupportedEncodingException e) {
                    // should never get here!
                    OutputStreamWriter osw = new OutputStreamWriter(os);
                    encoding = osw.getEncoding();
                    writer = new BufferedWriter(osw);
                }

                String typeName;
                int type = state.getType();
                try {
                    typeName = PropertyType.nameFromValue(type);
                } catch (IllegalArgumentException iae) {
                    // should never be getting here
                    throw new ItemStateException("unexpected property-type ordinal: " + type, iae);
                }

                writer.write("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>\n");
                writer.write("<" + PROPERTY_ELEMENT + " "
                        + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(state.getName().toString()) + "\" "
                        + PARENTUUID_ATTRIBUTE + "=\"" + state.getParentId() + "\" "
                        + MULTIVALUED_ATTRIBUTE + "=\"" + Boolean.toString(state.isMultiValued()) + "\" "
                        + MODCOUNT_ATTRIBUTE + "=\"" + state.getModCount() + "\" "
                        + TYPE_ATTRIBUTE + "=\"" + typeName + "\">\n");
                // values
                writer.write("\t<" + VALUES_ELEMENT + ">\n");
                InternalValue[] values = state.getValues();
                if (values != null) {
                    for (int i = 0; i < values.length; i++) {
                        writer.write("\t\t<" + VALUE_ELEMENT + ">");
                        InternalValue val = values[i];
                        if (val != null) {
                            if (type == PropertyType.BINARY) {
                                // special handling required for binary value:
                                // put binary value in BLOB store
                                InputStream in = val.getStream();
                                String blobId = blobStore.createId(state.getPropertyId(), i);
                                try {
                                    blobStore.put(blobId, in, val.getLength());
                                } finally {
                                    IOUtils.closeQuietly(in);
                                }
                                // store id of BLOB as property value
                                writer.write(blobId);
                                // replace value instance with value backed by resource
                                // in BLOB store and discard old value instance (e.g. temp file)
                                if (blobStore instanceof ResourceBasedBLOBStore) {
                                    // optimization: if the BLOB store is resource-based
                                    // retrieve the resource directly rather than having
                                    // to read the BLOB from an input stream
                                    FileSystemResource fsRes =
                                            ((ResourceBasedBLOBStore) blobStore).getResource(blobId);
                                    values[i] = InternalValue.create(fsRes);
                                } else {
                                    in = blobStore.get(blobId);
                                    try {
                                        values[i] = InternalValue.create(in);
                                    } finally {
                                        try {
                                            in.close();
                                        } catch (IOException e) {
                                            // ignore
                                        }
                                    }
                                }
                                val.discard();
                            } else {
                                writer.write(Text.encodeIllegalXMLCharacters(val.toString()));
                            }
                        }
                        writer.write("</" + VALUE_ELEMENT + ">\n");
                    }
                }
                writer.write("\t</" + VALUES_ELEMENT + ">\n");
                writer.write("</" + PROPERTY_ELEMENT + ">\n");
            } finally {
                writer.close();
            }
        } catch (Exception e) {
            String msg = "failed to store property state: " + state.getParentId() + "/" + state.getName();
            log.debug(msg);
            throw new ItemStateException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void destroy(NodeState state) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        NodeId id = state.getNodeId();
        String nodeFilePath = buildNodeFilePath(id);
        FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath);
        try {
            if (nodeFile.exists()) {
                // delete resource and prune empty parent folders
                nodeFile.delete(true);
            }
        } catch (FileSystemException fse) {
            String msg = "failed to delete node state: " + id;
            log.debug(msg);
            throw new ItemStateException(msg, fse);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void destroy(PropertyState state) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        // delete binary values (stored as files)
        InternalValue[] values = state.getValues();
        if (values != null) {
            for (int i = 0; i < values.length; i++) {
                InternalValue val = values[i];
                if (val != null) {
                    val.deleteBinaryResource();
                }
            }
        }
        // delete property file
        String propFilePath = buildPropFilePath(state.getPropertyId());
        FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath);
        try {
            if (propFile.exists()) {
                // delete resource and prune empty parent folders
                propFile.delete(true);
            }
        } catch (FileSystemException fse) {
            String msg = "failed to delete property state: " + state.getParentId() + "/" + state.getName();
            log.debug(msg);
            throw new ItemStateException(msg, fse);
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized NodeReferences loadReferencesTo(NodeId id)
            throws NoSuchItemStateException, ItemStateException {

        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        Exception e = null;
        String refsFilePath = buildNodeReferencesFilePath(id);
        try {
            if (!itemStateFS.isFile(refsFilePath)) {
                throw new NoSuchItemStateException(id.toString());
            }

            InputStream in = itemStateFS.getInputStream(refsFilePath);

            try {
                DOMWalker walker = new DOMWalker(in);
                NodeReferences refs = new NodeReferences(id);
                readState(walker, refs);
                return refs;
            } finally {
                in.close();
            }
        } catch (IOException ioe) {
            e = ioe;
            // fall through
        } catch (FileSystemException fse) {
            e = fse;
            // fall through
        }
        String msg = "failed to load references: " + id;
        log.debug(msg);
        throw new ItemStateException(msg, e);
    }

    /**
     * {@inheritDoc}
     */
    protected void store(NodeReferences refs) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        String refsFilePath = buildNodeReferencesFilePath(refs.getTargetId());
        FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath);
        try {
            refsFile.makeParentDirs();
            OutputStream os = refsFile.getOutputStream();
            BufferedWriter writer = null;
            try {
                String encoding = DEFAULT_ENCODING;
                try {
                    writer = new BufferedWriter(new OutputStreamWriter(os, encoding));
                } catch (UnsupportedEncodingException e) {
                    // should never get here!
                    OutputStreamWriter osw = new OutputStreamWriter(os);
                    encoding = osw.getEncoding();
                    writer = new BufferedWriter(osw);
                }
                writer.write("<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>\n");
                writer.write("<" + NODEREFERENCES_ELEMENT + " "
                        + TARGETID_ATTRIBUTE + "=\"" + refs.getTargetId() + "\">\n");
                // write references (i.e. the id's of the REFERENCE properties)
                for (PropertyId propId : refs.getReferences()) {
                    writer.write("\t<" + NODEREFERENCE_ELEMENT + " "
                            + PROPERTYID_ATTRIBUTE + "=\"" + propId + "\"/>\n");
                }
                writer.write("</" + NODEREFERENCES_ELEMENT + ">\n");
            } finally {
                writer.close();
            }
        } catch (Exception e) {
            String msg = "failed to store " + refs;
            log.debug(msg);
            throw new ItemStateException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void destroy(NodeReferences refs) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        String refsFilePath = buildNodeReferencesFilePath(refs.getTargetId());
        FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath);
        try {
            if (refsFile.exists()) {
                // delete resource and prune empty parent folders
                refsFile.delete(true);
            }
        } catch (FileSystemException fse) {
            String msg = "failed to delete " + refs;
            log.debug(msg);
            throw new ItemStateException(msg, fse);
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized boolean exists(NodeId id) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        try {
            String nodeFilePath = buildNodeFilePath(id);
            FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath);
            return nodeFile.exists();
        } catch (FileSystemException fse) {
            String msg = "failed to check existence of item state: " + id;
            log.debug(msg);
            throw new ItemStateException(msg, fse);
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized boolean exists(PropertyId id) throws ItemStateException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        try {
            String propFilePath = buildPropFilePath(id);
            FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath);
            return propFile.exists();
        } catch (FileSystemException fse) {
            String msg = "failed to check existence of item state: " + id;
            log.error(msg, fse);
            throw new ItemStateException(msg, fse);
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized boolean existsReferencesTo(NodeId id)
            throws ItemStateException {

        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        try {
            String refsFilePath = buildNodeReferencesFilePath(id);
            FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath);
            return refsFile.exists();
        } catch (FileSystemException fse) {
            String msg = "failed to check existence of references: " + id;
            log.debug(msg);
            throw new ItemStateException(msg, fse);
        }
    }
}
