/*
 * Copyright (C) 2008, 2016 XStream Committers.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 *
 * Created on 20. November 2008 by Joerg Schaible
 */
package com.thoughtworks.xstream.persistence;

import java.io.File;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.SingleValueConverter;
import com.thoughtworks.xstream.io.xml.DomDriver;


/**
 * PersistenceStrategy to assign keys with single value to objects persisted in files. The default naming strategy is
 * based on the key's type and its {@link SingleValueConverter}. It escapes all characters that are normally illegal in
 * the most common file systems. Such a character is escaped with percent escaping as it is done by URL encoding. The
 * XStream used to marshal the values is also requested for the key's SingleValueConverter. A
 * {@link ConversionException} is thrown if no such converter is registered.
 *
 * @author J&ouml;rg Schaible
 * @author Guilherme Silveira
 * @since 1.3.1
 */
public class FilePersistenceStrategy extends AbstractFilePersistenceStrategy {

    private final String illegalChars;

    /**
     * Create a new FilePersistenceStrategy. Use a standard XStream instance with a {@link DomDriver}.
     *
     * @param baseDirectory the directory for the serialized values
     * @since 1.3.1
     */
    public FilePersistenceStrategy(final File baseDirectory) {
        this(baseDirectory, new XStream(new DomDriver()));
    }

    /**
     * Create a new FilePersistenceStrategy with a provided XStream instance.
     *
     * @param baseDirectory the directory for the serialized values
     * @param xstream the XStream instance to use for (de)serialization
     * @since 1.3.1
     */
    public FilePersistenceStrategy(final File baseDirectory, final XStream xstream) {
        this(baseDirectory, xstream, "utf-8", "<>?:/\\\"|*%");
    }

    /**
     * Create a new FilePersistenceStrategy with a provided XStream instance and the characters to encode.
     *
     * @param baseDirectory the directory for the serialized values
     * @param xstream the XStream instance to use for (de)serialization
     * @param encoding encoding used to write the files
     * @param illegalChars illegal characters for file names (should always include '%' as long as you do not overwrite
     *            the (un)escape methods)
     * @since 1.3.1
     */
    public FilePersistenceStrategy(
            final File baseDirectory, final XStream xstream, final String encoding, final String illegalChars) {
        super(baseDirectory, xstream, encoding);
        this.illegalChars = illegalChars;
    }

    protected boolean isValid(final File dir, final String name) {
        return super.isValid(dir, name) && name.indexOf('@') > 0;
    }

    /**
     * Given a filename, the unescape method returns the key which originated it.
     *
     * @param name the filename
     * @return the original key
     */
    protected Object extractKey(final String name) {
        final String key = unescape(name.substring(0, name.length() - 4));
        if ("null@null".equals(key)) {
            return null;
        }
        final int idx = key.indexOf('@');
        if (idx < 0) {
            final ConversionException exception = new ConversionException("No valid key");
            exception.add("key", key);
            throw exception;
        }
        final Class type = getMapper().realClass(key.substring(0, idx));
        final Converter converter = getConverterLookup().lookupConverterForType(type);
        if (converter instanceof SingleValueConverter) {
            final SingleValueConverter svConverter = (SingleValueConverter)converter;
            return svConverter.fromString(key.substring(idx + 1));
        } else {
            final ConversionException exception = new ConversionException(
                "No SingleValueConverter available for key type");
            exception.add("key-type", type.getName());
            throw exception;
        }
    }

    protected String unescape(String name) {
        final StringBuffer buffer = new StringBuffer();
        for (int idx = name.indexOf('%'); idx >= 0; idx = name.indexOf('%')) {
            buffer.append(name.substring(0, idx));
            final int c = Integer.parseInt(name.substring(idx + 1, idx + 3), 16);
            buffer.append((char)c);
            name = name.substring(idx + 3);
        }
        buffer.append(name);
        return buffer.toString();
    }

    /**
     * Given a key, the escape method returns the filename which shall be used.
     *
     * @param key the key
     * @return the desired and escaped filename
     */
    protected String getName(final Object key) {
        if (key == null) {
            return "null@null.xml";
        }
        final Class type = key.getClass();
        final Converter converter = getConverterLookup().lookupConverterForType(type);
        if (converter instanceof SingleValueConverter) {
            final SingleValueConverter svConverter = (SingleValueConverter)converter;
            return getMapper().serializedClass(type) + '@' + escape(svConverter.toString(key)) + ".xml";
        } else {
            final ConversionException exception = new ConversionException(
                "No SingleValueConverter available for key type");
            exception.add("key-type", type.getName());
            throw exception;
        }
    }

    protected String escape(final String key) {
        final StringBuffer buffer = new StringBuffer();
        final char[] array = key.toCharArray();
        for (int i = 0; i < array.length; i++ ) {
            final char c = array[i];
            if (c >= ' ' && illegalChars.indexOf(c) < 0) {
                buffer.append(c);
            } else {
                buffer.append("%" + Integer.toHexString(c).toUpperCase());
            }
        }
        return buffer.toString();
    }
}
