/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.wc.admin;

import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.io.fs.FSFile;
import org.tmatesoft.svn.core.internal.util.*;
import org.tmatesoft.svn.core.internal.wc.*;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNTreeConflictDescription;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;

import java.io.*;
import java.util.*;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNAdminArea14 extends SVNAdminArea {

    public static final int WC_FORMAT = SVNAdminArea14Factory.WC_FORMAT;

    public static final String[] ourCachableProperties = new String[] {
        SVNProperty.SPECIAL,
        SVNProperty.EXTERNALS,
        SVNProperty.NEEDS_LOCK
    };

    protected static final String ATTRIBUTE_COPIED = "copied";
    protected static final String ATTRIBUTE_DELETED = "deleted";
    protected static final String ATTRIBUTE_ABSENT = "absent";
    protected static final String ATTRIBUTE_INCOMPLETE = "incomplete";
    protected static final String ATTRIBUTE_HAS_PROPS = "has-props";
    protected static final String ATTRIBUTE_HAS_PROP_MODS = "has-prop-mods";
    protected static final String KILL_ADM_ONLY = "adm-only";
    protected static final String THIS_DIR = "";

    private static final Set INAPPLICABLE_PROPERTIES = new SVNHashSet();

    static {
        INAPPLICABLE_PROPERTIES.add(SVNProperty.KEEP_LOCAL);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.CHANGELIST);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.WORKING_SIZE);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.DEPTH);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.PROP_TIME);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.FILE_EXTERNAL_PATH);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.FILE_EXTERNAL_REVISION);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.FILE_EXTERNAL_PEG_REVISION);
        INAPPLICABLE_PROPERTIES.add(SVNProperty.TREE_CONFLICT_DATA);
    }

    private File myLockFile;
    private File myEntriesFile;

    private static boolean ourIsOptimizedWritingEnabled;

    public SVNAdminArea14(File dir) {
        super(dir);
        myLockFile = new File(getAdminDirectory(), "lock");
        myEntriesFile = new File(getAdminDirectory(), "entries");
    }

    public static void setOptimizedWritingEnabled(boolean enabled) {
        ourIsOptimizedWritingEnabled = enabled;
    }

    public static String[] getCachableProperties() {
        return ourCachableProperties;
    }

    public void saveWCProperties(boolean close) throws SVNException {
        Map wcPropsCache = getWCPropertiesStorage(false);
        if (wcPropsCache == null) {
            return;
        }

        boolean hasAnyProps = false;
        File dstFile = getAdminFile("all-wcprops");
        File tmpFile = getAdminFile("tmp/all-wcprops");

        for(Iterator entries = wcPropsCache.keySet().iterator(); entries.hasNext();) {
            String name = (String)entries.next();
            SVNVersionedProperties props = (SVNVersionedProperties)wcPropsCache.get(name);
            if (!props.isEmpty()) {
                hasAnyProps = true;
                break;
            }
        }

        if (hasAnyProps) {
            OutputStream target = null;
            try {
                target = SVNFileUtil.openFileForWriting(tmpFile);
                SVNVersionedProperties props = (SVNVersionedProperties)wcPropsCache.get(getThisDirName());
                if (props != null && !props.isEmpty()) {
                    SVNWCProperties.setProperties(props.asMap(), target, SVNWCProperties.SVN_HASH_TERMINATOR);
                } else {
                    SVNWCProperties.setProperties(new SVNProperties(), target, SVNWCProperties.SVN_HASH_TERMINATOR);
                }

                for(Iterator entries = wcPropsCache.keySet().iterator(); entries.hasNext();) {
                    String name = (String)entries.next();
                    if (getThisDirName().equals(name)) {
                        continue;
                    }
                    props = (SVNVersionedProperties)wcPropsCache.get(name);
                    if (!props.isEmpty()) {
                        target.write(name.getBytes("UTF-8"));
                        target.write('\n');
                        SVNWCProperties.setProperties(props.asMap(), target, SVNWCProperties.SVN_HASH_TERMINATOR);
                    }
                }
            } catch (IOException ioe) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
                SVNErrorManager.error(err, ioe, SVNLogType.WC);
            } finally {
                SVNFileUtil.closeFile(target);
            }
            SVNFileUtil.rename(tmpFile, dstFile);
            SVNFileUtil.setReadonly(dstFile, true);
        } else {
            SVNFileUtil.deleteFile(dstFile);
        }
        if (close) {
            closeWCProperties();
        }
    }

    public SVNVersionedProperties getBaseProperties(String name) throws SVNException {
        Map basePropsCache = getBasePropertiesStorage(true);
        SVNVersionedProperties props = (SVNVersionedProperties)basePropsCache.get(name);
        if (props != null) {
            return props;
        }

        SVNProperties baseProps = null;
        try {
            baseProps = readBaseProperties(name);
        } catch (SVNException svne) {
            SVNErrorMessage err = svne.getErrorMessage().wrap("Failed to load properties from disk");
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }

        props = new SVNProperties13(baseProps);
        basePropsCache.put(name, props);
        return props;
    }

    public SVNVersionedProperties getRevertProperties(String name) throws SVNException {
        Map revertPropsCache = getRevertPropertiesStorage(true);
        SVNVersionedProperties props = (SVNVersionedProperties)revertPropsCache.get(name);
        if (props != null) {
            return props;
        }

        SVNProperties revertProps = null;
        try {
            revertProps = readRevertProperties(name);
        } catch (SVNException svne) {
            SVNErrorMessage err = svne.getErrorMessage().wrap("Failed to load properties from disk");
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }

        props = new SVNProperties13(revertProps);
        revertPropsCache.put(name, props);
        return props;
    }

    public SVNVersionedProperties getProperties(String name) throws SVNException {
        Map propsCache = getPropertiesStorage(true);
        SVNVersionedProperties props = (SVNVersionedProperties)propsCache.get(name);
        if (props != null) {
            return props;
        }

        final String entryName = name;
        props =  new SVNProperties14(null, this, name){

            protected SVNProperties loadProperties() throws SVNException {
                SVNProperties props = getProperties();
                if (props == null) {
                    try {
                        props = readProperties(entryName);
                    } catch (SVNException svne) {
                        SVNErrorMessage err = svne.getErrorMessage().wrap("Failed to load properties from disk");
                        SVNErrorManager.error(err, SVNLogType.DEFAULT);
                    }
                    props = props != null ? props : new SVNProperties();
                    setPropertiesMap(props);
                }
                return props;
            }
        };

        propsCache.put(name, props);
        return props;
    }

    public SVNVersionedProperties getWCProperties(String entryName) throws SVNException {
        SVNEntry entry = getEntry(entryName, false);
        if (entry == null) {
            return null;
        }

        Map wcPropsCache = getWCPropertiesStorage(true);
        SVNVersionedProperties props = (SVNVersionedProperties)wcPropsCache.get(entryName);
        if (props != null) {
            return props;
        }

        if (wcPropsCache.isEmpty()) {
            wcPropsCache = readAllWCProperties();
        }

        props = (SVNVersionedProperties)wcPropsCache.get(entryName);
        if (props == null) {
            props = new SVNProperties13(new SVNProperties());
            wcPropsCache.put(entryName, props);
        }
        return props;
    }

    private Map readAllWCProperties() throws SVNException {
        Map wcPropsCache = getWCPropertiesStorage(true);
        wcPropsCache.clear();
        File propertiesFile = getAdminFile("all-wcprops");
        if (!propertiesFile.exists()) {
            return wcPropsCache;
        }

        FSFile wcpropsFile = null;
        try {
            wcpropsFile = new FSFile(propertiesFile);
            SVNProperties wcProps = wcpropsFile.readProperties(false, true);
            SVNVersionedProperties entryWCProps = new SVNProperties13(wcProps);
            wcPropsCache.put(getThisDirName(), entryWCProps);

            String name = null;
            StringBuffer buffer = new StringBuffer();
            while(true) {
                try {
                    name = wcpropsFile.readLine(buffer);
                } catch (SVNException e) {
                    if (e.getErrorMessage().getErrorCode() == SVNErrorCode.STREAM_UNEXPECTED_EOF && buffer.length() > 0) {
                        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Missing end of line in wcprops file for ''{0}''", getRoot());
                        SVNErrorManager.error(err, e, SVNLogType.WC);
                    }
                    break;
                }
                wcProps = wcpropsFile.readProperties(false, true);
                entryWCProps = new SVNProperties13(wcProps);
                wcPropsCache.put(name, entryWCProps);
                buffer.delete(0, buffer.length());
            }
        } catch (SVNException svne) {
            SVNErrorMessage err = svne.getErrorMessage().wrap("Failed to load properties from disk");
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        } finally {
            wcpropsFile.close();
        }
        return wcPropsCache;
    }

    protected SVNProperties readBaseProperties(String name) throws SVNException {
        File propertiesFile = getBasePropertiesFile(name, false);
        SVNWCProperties props = new SVNWCProperties(propertiesFile, null);
        return props.asMap();
    }

    protected SVNProperties readRevertProperties(String name) throws SVNException {
        File propertiesFile = getRevertPropertiesFile(name, false);
        SVNWCProperties props = new SVNWCProperties(propertiesFile, null);
        return props.asMap();
    }

    protected SVNProperties readProperties(String name) throws SVNException {
        if (hasPropModifications(name)) {
            File propertiesFile = getPropertiesFile(name, false);
            SVNWCProperties props = new SVNWCProperties(propertiesFile, null);
            return props.asMap();
        }

        Map basePropsCache = getBasePropertiesStorage(true);
        if (basePropsCache != null ) {
            SVNVersionedProperties baseProps = (SVNVersionedProperties) basePropsCache.get(name);
            if (baseProps != null) {
                return baseProps.asMap();
            }
        }
        if (hasProperties(name)) {
            return readBaseProperties(name);
        }
        return new SVNProperties();
    }

    public void saveVersionedProperties(SVNLog log, boolean close) throws SVNException {
        SVNProperties command = new SVNProperties();
        Set processedEntries = new SVNHashSet();

        Map propsCache = getPropertiesStorage(false);
        if (propsCache != null && !propsCache.isEmpty()) {
            for(Iterator entries = propsCache.keySet().iterator(); entries.hasNext();) {
                String name = (String)entries.next();
                SVNVersionedProperties props = (SVNVersionedProperties)propsCache.get(name);
                if (props.isModified()) {
                    SVNVersionedProperties baseProps = getBaseProperties(name);
                    SVNVersionedProperties propsDiff = baseProps.compareTo(props);
                    String[] cachableProps = SVNAdminArea14.getCachableProperties();
                    command.put(SVNProperty.shortPropertyName(SVNProperty.CACHABLE_PROPS),
                            asString(cachableProps, " "));
                    SVNProperties propsMap = props.loadProperties();
                    LinkedList presentProps = new LinkedList();
                    for (int i = 0; i < cachableProps.length; i++) {
                        if (propsMap.containsName(cachableProps[i])) {
                            presentProps.addLast(cachableProps[i]);
                        }
                    }

                    if (presentProps.size() > 0) {
                        String presentPropsString = asString((String[])presentProps.toArray(new String[presentProps.size()]), " ");
                        command.put(SVNProperty.shortPropertyName(SVNProperty.PRESENT_PROPS), presentPropsString);
                    } else {
                        command.put(SVNProperty.shortPropertyName(SVNProperty.PRESENT_PROPS), "");
                    }

                    command.put(SVNProperty.shortPropertyName(SVNProperty.HAS_PROPS),
                            SVNProperty.toString(!props.isEmpty()));

                    boolean hasPropModifications = !propsDiff.isEmpty();
                    command.put(SVNProperty.shortPropertyName(SVNProperty.HAS_PROP_MODS), SVNProperty.toString(hasPropModifications));
                    command.put(SVNLog.NAME_ATTR, name);
                    log.addCommand(SVNLog.MODIFY_ENTRY, command, false);
                    processedEntries.add(name);
                    command.clear();

                    String dstPath = getThisDirName().equals(name) ? "dir-props" : "props/" + name + ".svn-work";
                    dstPath = getAdminDirectory().getName() + "/" + dstPath;

                    if (hasPropModifications) {
                        String tmpPath = "tmp/";
                        tmpPath += getThisDirName().equals(name) ? "dir-props" : "props/" + name + ".svn-work";
                        File tmpFile = getAdminFile(tmpPath);
                        String srcPath = getAdminDirectory().getName() + "/" + tmpPath;
                        SVNWCProperties tmpProps = new SVNWCProperties(tmpFile, srcPath);
                        if (!props.isEmpty()) {
                            tmpProps.setProperties(props.asMap());
                        } else {
                            SVNFileUtil.createEmptyFile(tmpFile);
                        }
                        command.put(SVNLog.NAME_ATTR, srcPath);
                        command.put(SVNLog.DEST_ATTR, dstPath);
                        log.addCommand(SVNLog.MOVE, command, false);
                        command.clear();
                        command.put(SVNLog.NAME_ATTR, dstPath);
                        log.addCommand(SVNLog.READONLY, command, false);
                    } else {
                        command.put(SVNLog.NAME_ATTR, dstPath);
                        log.addCommand(SVNLog.DELETE, command, false);
                    }
                    command.clear();
                    props.setModified(false);
                }
            }
        }

        Map basePropsCache = getBasePropertiesStorage(false);
        if (basePropsCache != null && !basePropsCache.isEmpty()) {
            for(Iterator entries = basePropsCache.keySet().iterator(); entries.hasNext();) {
                String name = (String)entries.next();
                SVNVersionedProperties baseProps = (SVNVersionedProperties)basePropsCache.get(name);
                if (baseProps.isModified()) {
                    String dstPath = getThisDirName().equals(name) ? "dir-prop-base" : "prop-base/" + name + ".svn-base";
                    dstPath = getAdminDirectory().getName() + "/" + dstPath;
                    boolean isEntryProcessed = processedEntries.contains(name);
                    if (!isEntryProcessed) {
                        SVNVersionedProperties props = getProperties(name);

                        String[] cachableProps = SVNAdminArea14.getCachableProperties();
                        command.put(SVNProperty.shortPropertyName(SVNProperty.CACHABLE_PROPS), asString(cachableProps, " "));

                        SVNProperties propsMap = props.loadProperties();
                        LinkedList presentProps = new LinkedList();
                        for (int i = 0; i < cachableProps.length; i++) {
                            if (propsMap.containsName(cachableProps[i])) {
                                presentProps.addLast(cachableProps[i]);
                            }
                        }

                        if (presentProps.size() > 0) {
                            String presentPropsString = asString((String[])presentProps.toArray(new String[presentProps.size()]), " ");
                            command.put(SVNProperty.shortPropertyName(SVNProperty.PRESENT_PROPS), presentPropsString);
                        } else {
                            command.put(SVNProperty.shortPropertyName(SVNProperty.PRESENT_PROPS), "");
                        }

                        command.put(SVNProperty.shortPropertyName(SVNProperty.HAS_PROPS), SVNProperty.toString(!props.isEmpty()));
                        SVNVersionedProperties propsDiff = baseProps.compareTo(props);
                        boolean hasPropModifications = !propsDiff.isEmpty();
                        command.put(SVNProperty.shortPropertyName(SVNProperty.HAS_PROP_MODS), SVNProperty.toString(hasPropModifications));
                        command.put(SVNLog.NAME_ATTR, name);
                        log.addCommand(SVNLog.MODIFY_ENTRY, command, false);
                        command.clear();

                        if (!hasPropModifications) {
                            String workingPropsPath = getThisDirName().equals(name) ? "dir-props" : "props/" + name + ".svn-work";
                            workingPropsPath = getAdminDirectory().getName() + "/" + workingPropsPath;
                            command.put(SVNLog.NAME_ATTR, workingPropsPath);
                            log.addCommand(SVNLog.DELETE, command, false);
                            command.clear();
                        }
                    }

                    if (baseProps.isEmpty()) {
                        command.put(SVNLog.NAME_ATTR, dstPath);
                        log.addCommand(SVNLog.DELETE, command, false);
                    } else {
                        String tmpPath = "tmp/";
                        tmpPath += getThisDirName().equals(name) ? "dir-prop-base" : "prop-base/" + name + ".svn-base";
                        File tmpFile = getAdminFile(tmpPath);
                        String srcPath = getAdminDirectory().getName() + "/" + tmpPath;
                        SVNWCProperties tmpProps = new SVNWCProperties(tmpFile, srcPath);
                        tmpProps.setProperties(baseProps.asMap());

                        command.put(SVNLog.NAME_ATTR, srcPath);
                        command.put(SVNLog.DEST_ATTR, dstPath);
                        log.addCommand(SVNLog.MOVE, command, false);
                        command.clear();
                        command.put(SVNLog.NAME_ATTR, dstPath);
                        log.addCommand(SVNLog.READONLY, command, false);
                    }
                    baseProps.setModified(false);
                }
            }
        }

        if (close) {
            closeVersionedProperties();
        }
    }

    public void installProperties(String name, SVNProperties baseProps, SVNProperties workingProps, SVNLog log,
            boolean writeBaseProps, boolean close) throws SVNException {
        SVNProperties command = new SVNProperties();
        SVNNodeKind kind = name.equals(getThisDirName()) ? SVNNodeKind.DIR : SVNNodeKind.FILE;
        SVNProperties propDiff = baseProps.compareTo(workingProps);

        boolean hasPropMods = !propDiff.isEmpty();
        command.put(SVNProperty.shortPropertyName(SVNProperty.HAS_PROP_MODS),
                SVNProperty.toString(hasPropMods));

        command.put(SVNProperty.shortPropertyName(SVNProperty.HAS_PROPS),
                SVNProperty.toString(!workingProps.isEmpty()));

        String[] cachableProps = SVNAdminArea14.getCachableProperties();
        command.put(SVNProperty.shortPropertyName(SVNProperty.CACHABLE_PROPS),
                asString(cachableProps, " "));

        LinkedList presentProps = new LinkedList();
        for (int i = 0; i < cachableProps.length; i++) {
            if (workingProps.containsName(cachableProps[i])) {
                presentProps.addLast(cachableProps[i]);
            }
        }

        if (presentProps.size() > 0) {
            String presentPropsString = asString((String[]) presentProps.toArray(new String[presentProps.size()]), " ");
            command.put(SVNProperty.shortPropertyName(SVNProperty.PRESENT_PROPS), presentPropsString);
        } else {
            command.put(SVNProperty.shortPropertyName(SVNProperty.PRESENT_PROPS), "");
        }

        command.put(SVNLog.NAME_ATTR, name);
        log.addCommand(SVNLog.MODIFY_ENTRY, command, false);
        command.clear();

        String dstPath = SVNAdminUtil.getPropPath(name, kind, false);

        if (hasPropMods) {
            String tmpPath = SVNAdminUtil.getPropPath(name, kind, true);
            File tmpFile = getFile(tmpPath);
            SVNWCProperties tmpProps = new SVNWCProperties(tmpFile, tmpPath);
            if (!workingProps.isEmpty()) {
                tmpProps.setProperties(workingProps);
            } else {
                SVNFileUtil.createEmptyFile(tmpFile);
            }
            command.put(SVNLog.NAME_ATTR, tmpPath);
            command.put(SVNLog.DEST_ATTR, dstPath);
            log.addCommand(SVNLog.MOVE, command, false);
            command.clear();
            command.put(SVNLog.NAME_ATTR, dstPath);
            log.addCommand(SVNLog.READONLY, command, false);
        } else {
            if (hasPropModifications(name)) {
                command.put(SVNLog.NAME_ATTR, dstPath);
                log.addCommand(SVNLog.DELETE, command, false);
            }
        }

        command.clear();

        if (writeBaseProps) {
            String basePath = SVNAdminUtil.getPropBasePath(name, kind, false);
            if (!baseProps.isEmpty()) {
                String tmpPath = SVNAdminUtil.getPropBasePath(name, kind, true);
                File tmpFile = getFile(tmpPath);
                SVNWCProperties tmpProps = new SVNWCProperties(tmpFile, tmpPath);
                tmpProps.setProperties(baseProps);

                command.put(SVNLog.NAME_ATTR, tmpPath);
                command.put(SVNLog.DEST_ATTR, basePath);
                log.addCommand(SVNLog.MOVE, command, false);
                command.clear();
                command.put(SVNLog.NAME_ATTR, basePath);
                log.addCommand(SVNLog.READONLY, command, false);
            } else {
                if (hasProperties(name)) {
                    command.put(SVNLog.NAME_ATTR, basePath);
                    log.addCommand(SVNLog.DELETE, command, false);
                }
            }
        }

        if (close) {
            closeVersionedProperties();
        }
    }

    public void handleKillMe() throws SVNException {
        boolean killMe = isKillMe();
        if (killMe) {
            String contents = SVNFileUtil.readFile(getAdminFile(ADM_KILLME));
            boolean killAdmOnly = KILL_ADM_ONLY.equals(contents);
            SVNEntry entry = getEntry(getThisDirName(), false);
            long dirRevision = entry != null ? entry.getRevision() : -1;
            // deleted dir, files and entry in parent.
            File dir = getRoot();
            SVNWCAccess access = getWCAccess();
            boolean isWCRoot = access.isWCRoot(getRoot());
            try {
                removeFromRevisionControl(getThisDirName(), !killAdmOnly, false);
            } catch (SVNException svne) {
                if (svne.getErrorMessage().getErrorCode() != SVNErrorCode.WC_LEFT_LOCAL_MOD) {
                    throw svne;
                }
            }
            if (isWCRoot) {
                return;
            }
            // compare revision with parent's one
            SVNAdminArea parentArea = access.retrieve(dir.getParentFile());
            SVNEntry parentEntry = parentArea.getEntry(parentArea.getThisDirName(), false);
            if (dirRevision > parentEntry.getRevision()) {
                SVNEntry entryInParent = parentArea.addEntry(dir.getName());
                Map attributes = new SVNHashMap();
                attributes.put(SVNProperty.DELETED, Boolean.TRUE.toString());
                attributes.put(SVNProperty.KIND, SVNProperty.KIND_DIR);
                attributes.put(SVNProperty.REVISION, Long.toString(dirRevision));
                parentArea.modifyEntry(entryInParent.getName(), attributes, true, false);
            }
        }
    }

    public void saveEntries(boolean close) throws SVNException {
        if (myEntries != null) {
            if (!isLocked()) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "No write-lock in ''{0}''", getRoot());
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }

            SVNEntry rootEntry = (SVNEntry) myEntries.get(getThisDirName());
            if (rootEntry == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "No default entry in directory ''{0}''", getRoot());
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }

            String reposURL = rootEntry.getRepositoryRoot();
            String url = rootEntry.getURL();
            if (reposURL != null && !SVNPathUtil.isAncestor(reposURL, url)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "Entry ''{0}'' has inconsistent repository root and url", getThisDirName());
                SVNErrorManager.error(err, SVNLogType.DEFAULT);
            }

            if (ourIsOptimizedWritingEnabled) {
                File tmpFile = new File(getAdminDirectory(), "tmp/entries");
                boolean renamed = myEntriesFile.renameTo(tmpFile);
                if (!renamed) {
                    myEntriesFile.delete();
                    tmpFile = null;
                }
                Writer os = null;
                try {
                    os = new OutputStreamWriter(SVNFileUtil.openFileForWriting(myEntriesFile), "UTF-8");
                    writeEntries(os);
                } catch (IOException e) {
                    SVNFileUtil.closeFile(os);
                    if (tmpFile != null) {
                        SVNFileUtil.rename(tmpFile, myEntriesFile);
                    }
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot write entries file ''{0}'': {1}", new Object[] {myEntriesFile, e.getLocalizedMessage()});
                    SVNErrorManager.error(err, e, SVNLogType.WC);
                } finally {
                    SVNFileUtil.closeFile(os);
                    SVNFileUtil.deleteFile(tmpFile);
                }
                SVNFileUtil.setReadonly(myEntriesFile, true);
            } else {
                File tmpFile = new File(getAdminDirectory(), "tmp/entries");
                Writer os = null;
                try {
                    os = new OutputStreamWriter(SVNFileUtil.openFileForWriting(tmpFile), "UTF-8");
                    writeEntries(os);
                } catch (IOException e) {
                    SVNFileUtil.closeFile(os);
                    SVNFileUtil.deleteFile(tmpFile);
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot write entries file ''{0}'': {1}", new Object[] {myEntriesFile, e.getMessage()});
                    SVNErrorManager.error(err, e, SVNLogType.WC);
                } finally {
                    SVNFileUtil.closeFile(os);
                }
                SVNFileUtil.setReadonly(tmpFile, true);
                SVNFileUtil.rename(tmpFile, myEntriesFile);
            }
            if (close) {
                closeEntries();
            }
        }
    }

    protected Map fetchEntries() throws SVNException {
        if (!myEntriesFile.exists()) {
            return null;
        }

        Map entries = new SVNHashMap();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(SVNFileUtil.openFileForReading(myEntriesFile, SVNLogType.WC), "UTF-8"));
            //skip format line
            reader.readLine();
            int entryNumber = 1;
            while(true){
                try {
                    SVNEntry entry = readEntry(reader, entryNumber);
                    if (entry == null) {
                        break;
                    }
                    entries.put(entry.getName(), entry);
                } catch (SVNException svne) {
                    SVNErrorMessage err = svne.getErrorMessage().wrap("Error at entry {0} in entries file for ''{1}'':", new Object[]{entryNumber, getRoot()});
                    SVNErrorManager.error(err, svne, SVNLogType.WC);
                }
                ++entryNumber;
            }
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot read entries file ''{0}'': {1}", new Object[] {myEntriesFile, e.getMessage()});
            SVNErrorManager.error(err, e, SVNLogType.WC);
        } finally {
            SVNFileUtil.closeFile(reader);
        }

        SVNEntry defaultEntry = (SVNEntry)entries.get(getThisDirName());
        if (defaultEntry == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, "Missing default entry");
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (defaultEntry.getRevision() < 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_REVISION, "Default entry has no revision number");
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (defaultEntry.getURL() == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_MISSING_URL, "Default entry is missing URL");
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        for (Iterator entriesIter = entries.keySet().iterator(); entriesIter.hasNext();) {
            String name = (String)entriesIter.next();
            SVNEntry entry = (SVNEntry)entries.get(name);
            if (getThisDirName().equals(name)) {
                continue;
            }

            SVNNodeKind kind = entry.getKind();
            if (kind == SVNNodeKind.FILE) {
                if (entry.getRevision() < 0) {
                    entry.setRevision(defaultEntry.getRevision());
                }
                if (entry.getURL() == null) {
                    entry.setParentURL(defaultEntry.getURL());
                }
                if (entry.getRepositoryRoot() == null) {
                    entry.setRepositoryRoot(defaultEntry.getRepositoryRoot());
                }
                if (entry.getUUID() == null) {
                    if (!(entry.isScheduledForAddition() || entry.isScheduledForReplacement())) {
                        entry.setUUID(defaultEntry.getUUID());
                    }
                }
                if (entry.getCachableProperties() == null) {
                    entry.setCachableProperties(defaultEntry.getCachableProperties());
                }
            }
        }
        
        return entries;
    }

    protected SVNEntry readEntry(BufferedReader reader, int entryNumber) throws IOException, SVNException {
        String line = reader.readLine();
        if (line == null && entryNumber > 1) {
            return null;
        }

        String name = parseString(line);
        name = name != null ? name : getThisDirName();

        name = (String) getObjectsPool().getObject(name);
        SVNEntry entry = new SVNEntry16(this, name);
        entry.setDepth(SVNDepth.INFINITY);

        line = reader.readLine();
        String kind = parseValue(line);
        if (kind != null) {
            SVNNodeKind parsedKind = SVNNodeKind.parseKind(kind);
            if (parsedKind != SVNNodeKind.UNKNOWN && parsedKind != SVNNodeKind.NONE) {
                entry.setKind(parsedKind);
            } else {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.NODE_UNKNOWN_KIND, "Entry ''{0}'' has invalid node kind", name);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        } else {
            entry.setKind(SVNNodeKind.NONE);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String revision = parseValue(line);
        if (revision != null) {
            try {
                long rev = Long.parseLong(revision);
                entry.setRevision(rev);
            } catch (NumberFormatException nfe) {
                entry.setRevision(SVNRepository.INVALID_REVISION);
            }
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String url = parseString(line);
        if (url != null) {
            entry.setURL(url);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String reposRoot = parseString(line);
        if (reposRoot != null && url != null && !SVNPathUtil.isAncestor(reposRoot, url)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Entry for ''{0}'' has invalid repository root", name);
            SVNErrorManager.error(err, SVNLogType.WC);
        } else if (reposRoot != null) {
            reposRoot = (String) getObjectsPool().getObject(reposRoot);
            entry.setRepositoryRoot(reposRoot);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String schedule = parseValue(line);
        if (schedule != null) {
            if (SVNProperty.SCHEDULE_ADD.equals(schedule) || SVNProperty.SCHEDULE_DELETE.equals(schedule) || SVNProperty.SCHEDULE_REPLACE.equals(schedule)) {
                entry.setSchedule(schedule);
            } else {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_ATTRIBUTE_INVALID, "Entry ''{0}'' has invalid ''{1}'' value", new Object[]{name, SVNProperty.SCHEDULE});
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String timestamp = parseValue(line);
        if (timestamp != null) {
            entry.setTextTime(timestamp);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String checksum = parseString(line);
        if (checksum != null) {
            entry.setChecksum(checksum);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String committedDate = parseValue(line);
        if (committedDate != null) {
            entry.setCommittedDate(committedDate);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String committedRevision = parseValue(line);
        if (committedRevision != null) {
            try {
                long rev = Long.parseLong(committedRevision);
                entry.setCommittedRevision(rev);
            } catch (NumberFormatException nfe) {
                entry.setCommittedRevision(SVNRepository.INVALID_REVISION);
            }
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String committedAuthor = parseString(line);
        if (committedAuthor != null) {
            committedAuthor = (String) getObjectsPool().getObject(committedAuthor);
            entry.setAuthor(committedAuthor);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        boolean hasProps = parseBoolean(line, ATTRIBUTE_HAS_PROPS);
        if (hasProps && entry instanceof SVNEntry16) {
            ((SVNEntry) entry).setHasProperties(hasProps);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        boolean hasPropMods = parseBoolean(line, ATTRIBUTE_HAS_PROP_MODS);
        if (hasPropMods && entry instanceof SVNEntry16) {
            ((SVNEntry) entry).setHasPropertiesModifications(hasPropMods);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String cachablePropsStr = parseValue(line);
        if (cachablePropsStr != null) {
            String[] cachableProps = fromString(cachablePropsStr, " ");
            if (Arrays.equals(cachableProps, getCachableProperties())) {
                cachableProps = getCachableProperties();
            }
            entry.setCachableProperties(cachableProps);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String presentPropsStr = parseValue(line);
        if (presentPropsStr != null && entry instanceof SVNEntry16) {
            String[] presentProps = fromString(presentPropsStr, " ");
            ((SVNEntry) entry).setPresentProperties(presentProps);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String prejFile = parseString(line);
        if (prejFile != null) {
            entry.setPropRejectFile(prejFile);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String conflictOldFile = parseString(line);
        if (conflictOldFile != null) {
            entry.setConflictOld(conflictOldFile);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String conflictNewFile = parseString(line);
        if (conflictNewFile != null) {
            entry.setConflictNew(conflictNewFile);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String conflictWorkFile = parseString(line);
        if (conflictWorkFile != null) {
            entry.setConflictWorking(conflictWorkFile);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        boolean isCopied = parseBoolean(line, ATTRIBUTE_COPIED);
        if (isCopied) {
            entry.setCopied(isCopied);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String copyfromURL = parseString(line);
        if (copyfromURL != null) {
            entry.setCopyFromURL(copyfromURL);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String copyfromRevision = parseValue(line);
        if (copyfromRevision != null) {
            try {
                long rev = Long.parseLong(copyfromRevision);
                entry.setCopyFromRevision(rev);
            } catch (NumberFormatException nfe) {
                entry.setCopyFromRevision(SVNRepository.INVALID_REVISION);
            }
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        boolean isDeleted = parseBoolean(line, ATTRIBUTE_DELETED);
        if (isDeleted) {
            entry.setDeleted(isDeleted);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        boolean isAbsent = parseBoolean(line, ATTRIBUTE_ABSENT);
        if (isAbsent) {
            entry.setAbsent(isAbsent);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        boolean isIncomplete = parseBoolean(line, ATTRIBUTE_INCOMPLETE);
        if (isIncomplete) {
            entry.setIncomplete(isIncomplete);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String uuid = parseString(line);
        if (uuid != null) {
            uuid = (String) getObjectsPool().getObject(uuid);
            entry.setUUID(uuid);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String lockToken = parseString(line);
        if (lockToken != null) {
            entry.setLockToken(lockToken);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String lockOwner = parseString(line);
        if (lockOwner != null) {
            lockOwner = (String) getObjectsPool().getObject(lockOwner);
            entry.setLockOwner(lockOwner);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String lockComment = parseString(line);
        if (lockComment != null) {
            entry.setLockComment(lockComment);
        }

        line = reader.readLine();
        if (isEntryFinished(line)) {
            return entry;
        }
        String lockCreationDate = parseValue(line);
        if (lockCreationDate != null) {
            entry.setLockCreationDate(lockCreationDate);
        }

        if (readExtraOptions(reader, entry)) {
            return entry;
        }

        do {
            line = reader.readLine();
            if (line == null) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Missing entry terminator");
                SVNErrorManager.error(err, SVNLogType.WC);
            } else if (line.length() == 1 && line.charAt(0) == '\f') {
                break;
            }
        } while (line != null);

        return entry;
    }

    protected boolean isEntryFinished(String line) {
        return line != null && line.length() > 0 && line.charAt(0) == '\f';
    }

    protected boolean parseBoolean(String line, String field) throws SVNException {
        line = parseValue(line);
        if (line != null) {
            if (!line.equals(field)) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Invalid value for field ''{0}''", field);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            return true;
        }
        return false;
    }

    protected String parseString(String line) throws SVNException {
        if (line == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Unexpected end of entry");
            SVNErrorManager.error(err, SVNLogType.WC);
        } else if ("".equals(line)) {
            return null;
        }

        int fromIndex = 0;
        int ind = -1;
        StringBuffer buffer = null;
        String escapedString = null;
        while ((ind = line.indexOf('\\', fromIndex)) != -1) {
            if (line.length() < ind + 4 || line.charAt(ind + 1) != 'x' || !SVNEncodingUtil.isHexDigit(line.charAt(ind + 2)) || !SVNEncodingUtil.isHexDigit(line.charAt(ind + 3))) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Invalid escape sequence");
                SVNErrorManager.error(err, SVNLogType.WC);
            }
            if (buffer == null) {
                buffer = new StringBuffer();
            }

            escapedString = line.substring(ind + 2, ind + 4);
            int escapedByte = Integer.parseInt(escapedString, 16);

            if (ind > fromIndex) {
                buffer.append(line.substring(fromIndex, ind));
                buffer.append((char)(escapedByte & 0xFF));
            } else {
                buffer.append((char)(escapedByte & 0xFF));
            }
            fromIndex = ind + 4;
        }

        if (buffer != null) {
            if (fromIndex < line.length()) {
                buffer.append(line.substring(fromIndex));
            }
            return buffer.toString();
        }
        return line;
    }

    protected String parseValue(String line) throws SVNException {
        if (line == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Unexpected end of entry");
            SVNErrorManager.error(err, SVNLogType.WC);
        } else if ("".equals(line)) {
            return null;
        }
        return line;
    }

    public String getThisDirName() {
        return THIS_DIR;
    }

    protected boolean readExtraOptions(BufferedReader reader, SVNEntry entry) throws SVNException, IOException {
        return false;
    }

    protected void writeEntries(Writer writer) throws IOException, SVNException {
        SVNEntry rootEntry = (SVNEntry)myEntries.get(getThisDirName());
        writer.write(getFormatVersion() + "\n");
        writeEntry(writer, getThisDirName(), rootEntry, null);

        List names = new ArrayList(myEntries.keySet());
        Collections.sort(names);
        for (Iterator entries = names.iterator(); entries.hasNext();) {
            String name = (String)entries.next();
            SVNEntry entry = (SVNEntry)myEntries.get(name);
            if (getThisDirName().equals(name)) {
                continue;
            }

            SVNNodeKind kind = entry.getKind();
            if (kind == SVNNodeKind.FILE) {
                if (entry.getRevision() < 0) {
                    entry.setRevision(rootEntry.getRevision());
                }
                if (entry.getURL() == null) {
                    entry.setParentURL(rootEntry.getURL());
                }
                if (entry.getRepositoryRoot() == null) {
                    entry.setRepositoryRoot(rootEntry.getRepositoryRoot());
                }
                if (entry.getUUID() == null) {
                    if (!(entry.isScheduledForAddition() || entry.isScheduledForReplacement())) {
                        entry.setUUID(rootEntry.getUUID());
                    }
                }
                if (entry.getCachableProperties() == null) {
                    entry.setCachableProperties(rootEntry.getCachableProperties());
                }
            }
            writeEntry(writer, name, entry, rootEntry);
        }
    }

    private void writeEntry(Writer writer, String name, SVNEntry entry, SVNEntry rootEntry) throws IOException, SVNException {
        boolean isThisDir = getThisDirName().equals(name);
        boolean isSubDir = !isThisDir && entry.isDirectory();
        int emptyFields = 0;

        if (!writeString(writer, name, emptyFields)) {
            ++emptyFields;
        }

        String kind = (String) entry.getKind().toString();
        if (writeValue(writer, kind, emptyFields)){
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        long revision = -1;
        if (isThisDir) {
            revision = entry.getRevision();
        } else if (!isSubDir) {
            revision = entry.getRevision();
            if (revision == rootEntry.getRevision()) {
                revision = -1;
            }
        }
        if (writeRevision(writer, revision, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String url = null;
        if (isThisDir) {
            url = entry.getURL();
        } else if (!isSubDir) {
            url = entry.getURL();
            String expectedURL = SVNPathUtil.append(rootEntry.getURL(), SVNEncodingUtil.uriEncode(name));
            if (url != null && url.equals(expectedURL)) {
                url = null;
            }
        }
        if (writeString(writer, url, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String root = null;
        if (isThisDir) {
            root = (String)entry.getRepositoryRoot();
        } else if (!isSubDir) {
            String thisDirRoot = rootEntry.getRepositoryRoot();
            root = (String)entry.getRepositoryRoot();
            if (root != null && root.equals(thisDirRoot)) {
                root = null;
            }
        }
        if (writeString(writer, root, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String schedule = entry.getSchedule();
        if (schedule != null && (!SVNProperty.SCHEDULE_ADD.equals(schedule) && !SVNProperty.SCHEDULE_DELETE.equals(schedule) && !SVNProperty.SCHEDULE_REPLACE.equals(schedule))) {
            schedule = null;
        }
        if (writeValue(writer, schedule, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String textTime = (String)entry.getTextTime();
        if (writeTime(writer, textTime, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String checksum = (String)entry.getChecksum();
        if (writeValue(writer, checksum, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String committedDate = (String)entry.getCommittedDate();
        if (writeTime(writer, committedDate, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        long committedRevision = entry.getCommittedRevision();
        if (writeRevision(writer, committedRevision, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String committedAuthor = (String)entry.getAuthor();
        if (writeString(writer, committedAuthor, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        boolean hasProps = entry.hasProperties();
        if (hasProps) {
            writeValue(writer, ATTRIBUTE_HAS_PROPS, emptyFields);
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        boolean hasPropsMods = entry.hasPropertiesModifications();
        if (hasPropsMods) {
            writeValue(writer, ATTRIBUTE_HAS_PROP_MODS, emptyFields);
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String cachableProps = asString(entry.getCachableProperties(), " ");
        if (!isThisDir) {
            String thisDirCachableProps = asString(rootEntry.getCachableProperties(), " ");
            if (thisDirCachableProps != null && cachableProps != null && thisDirCachableProps.equals(cachableProps)) {
                cachableProps = null;
            }
        }
        if (writeValue(writer, cachableProps, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String presentProps = asString(entry.getPresentProperties(), " ");
        if (writeValue(writer, presentProps, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String propRejectFile = entry.getPropRejectFile();
        if (writeString(writer, propRejectFile, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String conflictOldFile = entry.getConflictOld();
        if (writeString(writer, conflictOldFile, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String conflictNewFile = entry.getConflictNew();
        if (writeString(writer, conflictNewFile, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String conflictWrkFile = entry.getConflictWorking();
        if (writeString(writer, conflictWrkFile, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        boolean copiedAttr = entry.isCopied();
        if (copiedAttr) {
            writeValue(writer, ATTRIBUTE_COPIED, emptyFields);
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String copyfromURL = entry.getCopyFromURL();
        if (writeString(writer, copyfromURL, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        long copyfromRevision = entry.getCopyFromRevision();
        if (writeRevision(writer, copyfromRevision, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        boolean deletedAttr = entry.isDeleted();
        if (deletedAttr) {
            writeValue(writer, ATTRIBUTE_DELETED, emptyFields);
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        boolean absentAttr = entry.isAbsent();
        if (absentAttr) {
            writeValue(writer, ATTRIBUTE_ABSENT, emptyFields);
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        boolean incompleteAttr = entry.isIncomplete();
        if (incompleteAttr) {
            writeValue(writer, ATTRIBUTE_INCOMPLETE, emptyFields);
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String uuid = entry.getUUID();
        if (!isThisDir) {
            String thisDirUUID = rootEntry.getUUID();
            if (thisDirUUID != null && uuid != null && thisDirUUID.equals(uuid)) {
                uuid = null;
            }
        }
        if (writeValue(writer, uuid, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String lockToken = entry.getLockToken();
        if (writeString(writer, lockToken, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String lockOwner = entry.getLockOwner();
        if (writeString(writer, lockOwner, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String lockComment = entry.getLockComment();
        if (writeString(writer, lockComment, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        String lockCreationDate = entry.getLockCreationDate();
        if (writeTime(writer, lockCreationDate, emptyFields)) {
            emptyFields = 0;
        } else {
            ++emptyFields;
        }

        writeExtraOptions(writer, name, entry, emptyFields);
        writer.write("\f\n");
        writer.flush();
    }

    protected int writeExtraOptions(Writer writer, String entryName, SVNEntry entry, int emptyFields) throws SVNException, IOException {
        return emptyFields;
    }

    protected boolean writeString(Writer writer, String str, int emptyFields) throws IOException {
        if (str != null && str.length() > 0) {
            for (int i = 0; i < emptyFields; i++) {
                writer.write('\n');
            }
            for (int i = 0; i < str.length(); i++) {
                char ch = str.charAt(i);
                if (SVNEncodingUtil.isASCIIControlChar(ch) || ch == '\\') {
                    writer.write("\\x");
                    writer.write(SVNFormatUtil.getHexNumberFromByte((byte)ch));
                } else {
                    writer.write(ch);
                }
            }
            writer.write('\n');
            return true;
        }
        return false;
    }

    protected boolean writeValue(Writer writer, String val, int emptyFields) throws IOException {
        if (val != null && val.length() > 0) {
            for (int i = 0; i < emptyFields; i++) {
                writer.write('\n');
            }
            writer.write(val);
            writer.write('\n');
            return true;
        }
        return false;
    }

    protected boolean writeTime(Writer writer, String val, int emptyFields) throws IOException {
        if (val != null && val.length() > 0) {
            long time = SVNDate.parseDateAsMilliseconds(val);
            if (time > 0) {
                for (int i = 0; i < emptyFields; i++) {
                    writer.write('\n');
                }
                writer.write(val);
                writer.write('\n');
                return true;
            }
        }
        return false;
    }

    protected boolean writeRevision(Writer writer, long revValue, int emptyFields) throws IOException {
        if (revValue >= 0) {
            for (int i = 0; i < emptyFields; i++) {
                writer.write('\n');
            }
            writer.write(Long.toString(revValue));
            writer.write('\n');
            return true;
        }
        return false;
    }

    public boolean hasPropModifications(String name) throws SVNException {
        SVNEntry entry = getEntry(name, true);
        if (entry != null) {
            return entry.hasPropertiesModifications();
        }
        return false;
    }

    public boolean hasProperties(String name) throws SVNException {
        SVNEntry entry = getEntry(name, true);
        if (entry != null) {
            return entry.hasProperties();
        }
        return false;
    }

    public boolean lock() throws SVNException {
        return lock(false);
    }

    public boolean lock(boolean stealLock) throws SVNException {
        if (!isVersioned()) {
            return false;
        }
        if (stealLock && myLockFile.isFile()) {
            setLocked(true);
            return true;
        }
        boolean created = false;
        try {
            // Stian was here.
            created = SVNFileUtil.createNewFile(myLockFile);
        } catch (IOException e) {
            SVNErrorCode code = e.getMessage().indexOf("denied") >= 0 ? SVNErrorCode.WC_LOCKED : SVNErrorCode.WC_NOT_LOCKED;
            SVNErrorMessage err = SVNErrorMessage.create(code, "Cannot lock working copy ''{0}'': {1}",
                    new Object[] {getRoot(), e.getLocalizedMessage()});
            SVNErrorManager.error(err, e, SVNLogType.WC);
        }
        if (created) {
            setLocked(true);
            return created;
        }
        if (myLockFile.isFile()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_LOCKED, "Working copy ''{0}'' locked; try performing ''cleanup''", getRoot());
            SVNErrorManager.error(err, SVNLogType.WC);
        } else {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_NOT_LOCKED, "Cannot lock working copy ''{0}''", getRoot());
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return false;
    }

    public SVNAdminArea createVersionedDirectory(File dir, String url, String rootURL, String uuid,
            long revNumber, boolean createMyself, SVNDepth depth) throws SVNException {
        dir = createMyself ? getRoot() : dir;
        dir.mkdirs();
        File adminDir = createMyself ? getAdminDirectory() : new File(dir, SVNFileUtil.getAdminDirectoryName());
        adminDir.mkdir();
        SVNFileUtil.setHidden(adminDir, true);
        // lock dir.
        File lockFile = createMyself ? myLockFile : new File(adminDir, "lock");
        SVNFileUtil.createEmptyFile(lockFile);

        File[] tmp = {
                createMyself ? getAdminFile("tmp") : new File(adminDir, "tmp"),
                createMyself ? getAdminFile("tmp" + File.separatorChar + "props") : new File(adminDir, "tmp" + File.separatorChar + "props"),
                createMyself ? getAdminFile("tmp" + File.separatorChar + "prop-base") : new File(adminDir, "tmp" + File.separatorChar + "prop-base"),
                createMyself ? getAdminFile("tmp" + File.separatorChar + "text-base") : new File(adminDir, "tmp" + File.separatorChar + "text-base"),
                createMyself ? getAdminFile("props") : new File(adminDir, "props"),
                createMyself ? getAdminFile("prop-base") : new File(adminDir, "prop-base"),
                createMyself ? getAdminFile("text-base") : new File(adminDir, "text-base")
                };

        for (int i = 0; i < tmp.length; i++) {
            tmp[i].mkdir();
        }
        // for backward compatibility
        createFormatFile(createMyself ? null : new File(adminDir, "format"), createMyself);

        SVNAdminArea adminArea = createMyself ? this : createAdminAreaForDir(dir);
        adminArea.setLocked(true);
        SVNEntry rootEntry = adminArea.getEntry(adminArea.getThisDirName(), true);
        if (rootEntry == null) {
            rootEntry = adminArea.addEntry(adminArea.getThisDirName());
        }
        if (url != null) {
            rootEntry.setURL(url);
        }
        rootEntry.setRepositoryRoot(rootURL);
        rootEntry.setRevision(revNumber);
        rootEntry.setKind(SVNNodeKind.DIR);
        rootEntry.setDepth(depth);
        if (uuid != null) {
            rootEntry.setUUID(uuid);
        }
        if (revNumber > 0) {
            rootEntry.setIncomplete(true);
        }
        rootEntry.setCachableProperties(ourCachableProperties);
        try {
            adminArea.saveEntries(false);
        } catch (SVNException svne) {
            SVNErrorMessage err = svne.getErrorMessage().wrap("Error writing entries file for ''{0}''", dir);
            SVNErrorManager.error(err, svne, SVNLogType.WC);
        }

        // unlock dir.
        SVNFileUtil.deleteFile(lockFile);
        return adminArea;
    }

    protected SVNVersionedProperties formatBaseProperties(SVNProperties srcProperties) {
        SVNProperties props = new SVNProperties(srcProperties);
        return new SVNProperties13(props);
    }

    protected SVNVersionedProperties formatProperties(SVNEntry entry, SVNProperties srcProperties) {
        SVNProperties props = new SVNProperties(srcProperties);
        return new SVNProperties14(props, this, entry.getName()) {

            protected SVNProperties loadProperties() throws SVNException {
                return getProperties();
            }
        };
    }

    private void makeKillMe(boolean killAdminOnly) throws SVNException {
        File killMe = getAdminFile(ADM_KILLME);
        if (killMe.getParentFile().isDirectory()) {
            SVNFileUtil.createFile(killMe, killAdminOnly ? KILL_ADM_ONLY : null, null);
        }
    }

    public void postCommit(String fileName, long revisionNumber, boolean implicit, boolean rerun, SVNErrorCode errorCode) throws SVNException {
        SVNEntry entry = getEntry(fileName, true);
        if (entry == null || (!getThisDirName().equals(fileName) && entry.getKind() != SVNNodeKind.FILE)) {
            SVNErrorMessage err = SVNErrorMessage.create(errorCode, "Log command for directory ''{0}'' is mislocated", getRoot());
            SVNErrorManager.error(err, SVNLogType.WC);
        }

        if (!implicit && entry.isScheduledForDeletion()) {
            if (getThisDirName().equals(fileName)) {
                entry.setRevision(revisionNumber);
                entry.setKind(SVNNodeKind.DIR);
                if (rerun) {
                    File killMe = getAdminFile(ADM_KILLME);
                    if (killMe.isFile()) {
                        return;
                    }
                }
                makeKillMe(entry.isKeepLocal());
            } else {
                removeFromRevisionControl(fileName, false, false);
                SVNEntry parentEntry = getEntry(getThisDirName(), true);
                if (revisionNumber > parentEntry.getRevision()) {
                    SVNEntry fileEntry = addEntry(fileName);
                    fileEntry.setKind(SVNNodeKind.FILE);
                    fileEntry.setDeleted(true);
                    fileEntry.setRevision(revisionNumber);
                }
            }
            return;
        }

        if (!implicit && entry.isScheduledForReplacement() && getThisDirName().equals(fileName)) {
            for (Iterator ents = entries(true); ents.hasNext();) {
                SVNEntry currentEntry = (SVNEntry) ents.next();
                if (!currentEntry.isScheduledForDeletion()) {
                    continue;
                }
                if (currentEntry.getKind() == SVNNodeKind.FILE || currentEntry.getKind() == SVNNodeKind.DIR) {
                    removeFromRevisionControl(currentEntry.getName(), false, false);
                }
            }
        }

        long fileLength = 0;
        if (!getThisDirName().equals(fileName)) {
            File workingFile = getFile(fileName);
            fileLength = SVNFileUtil.getFileLength(workingFile);
        }

        long textTime = 0;
        if (!implicit && !getThisDirName().equals(fileName)) {
            File tmpFile = getBaseFile(fileName, true);
            SVNFileType fileType = SVNFileType.getType(tmpFile);
            if (fileType == SVNFileType.FILE || fileType == SVNFileType.SYMLINK) {
                boolean modified = false;
                File workingFile = getFile(fileName);
                long tmpTimestamp = SVNFileUtil.getFileLastModified(tmpFile);
                long wkTimestamp = SVNFileUtil.getFileLastModified(workingFile);
                if (tmpTimestamp != wkTimestamp) {
                    // check if wc file is not modified
                    File tmpFile2 = SVNFileUtil.createUniqueFile(tmpFile.getParentFile(), fileName, ".tmp", true);
                    try {
                        String tmpFile2Path = SVNFileUtil.getBasePath(tmpFile2);
                        SVNTranslator.translate(this, fileName, fileName, tmpFile2Path, false);
                        modified = !SVNFileUtil.compareFiles(tmpFile, tmpFile2, null);
                    } catch (SVNException svne) {
                        SVNErrorMessage err = SVNErrorMessage.create(errorCode, "Error comparing ''{0}'' and ''{1}''", new Object[] {workingFile, tmpFile});
                        SVNErrorManager.error(err, svne, SVNLogType.WC);
                    } finally {
                        tmpFile2.delete();
                    }
                }

                textTime = modified ? tmpTimestamp : wkTimestamp;
            }
        }
        if (!implicit && entry.isScheduledForReplacement()) {
            SVNFileUtil.deleteFile(getBasePropertiesFile(fileName, false));
        }

        boolean setReadWrite = false;
        boolean setNotExecutable = false;
        SVNVersionedProperties baseProps = getBaseProperties(fileName);
        SVNVersionedProperties wcProps = getProperties(fileName);

        //TODO: to work properly we must create a tmp working props file
        //instead of tmp base props one
        File tmpPropsFile = getPropertiesFile(fileName, true);
        File wcPropsFile = getPropertiesFile(fileName, false);
        File basePropertiesFile = getBasePropertiesFile(fileName, false);
        SVNFileType tmpPropsType = SVNFileType.getType(tmpPropsFile);
        // tmp may be missing when there were no prop change at all!
        if (tmpPropsType == SVNFileType.FILE) {
            if (!getThisDirName().equals(fileName)) {
                SVNVersionedProperties propDiff = baseProps.compareTo(wcProps);
                setReadWrite = propDiff != null && propDiff.containsProperty(SVNProperty.NEEDS_LOCK)
                        && propDiff.getPropertyValue(SVNProperty.NEEDS_LOCK) == null;
                setNotExecutable = propDiff != null
                        && propDiff.containsProperty(SVNProperty.EXECUTABLE)
                        && propDiff.getPropertyValue(SVNProperty.EXECUTABLE) == null;
            }
            try {
                if (!tmpPropsFile.exists() || tmpPropsFile.length() <= 4) {
                    SVNFileUtil.deleteFile(basePropertiesFile);
                } else {
                    SVNFileUtil.copyFile(tmpPropsFile, basePropertiesFile, true);
                    SVNFileUtil.setReadonly(basePropertiesFile, true);
                }
            } finally {
                SVNFileUtil.deleteFile(tmpPropsFile);
            }
        }

        if (!getThisDirName().equals(fileName) && !implicit) {
            File tmpFile = getBaseFile(fileName, true);
            File baseFile = getBaseFile(fileName, false);
            File wcFile = getFile(fileName);
            File tmpFile2 = null;
            try {
                tmpFile2 = SVNFileUtil.createUniqueFile(tmpFile.getParentFile(), fileName, ".tmp", false);
                boolean overwritten = false;
                SVNFileType fileType = SVNFileType.getType(tmpFile);
                boolean special = getProperties(fileName).getPropertyValue(SVNProperty.SPECIAL) != null;
                if (!SVNFileUtil.symlinksSupported() || !special) {
                    if (fileType == SVNFileType.FILE) {
                        SVNTranslator.translate(this, fileName,
                                SVNFileUtil.getBasePath(tmpFile), SVNFileUtil.getBasePath(tmpFile2), true);
                    } else {
                        SVNTranslator.translate(this, fileName, fileName,
                                SVNFileUtil.getBasePath(tmpFile2), true, true);
                    }
                    if (!SVNFileUtil.compareFiles(tmpFile2, wcFile, null)) {
                        SVNFileUtil.copyFile(tmpFile2, wcFile, true);
                        overwritten = true;
                    }
                }
                boolean needsReadonly = getProperties(fileName).getPropertyValue(SVNProperty.NEEDS_LOCK) != null && entry.getLockToken() == null;
                boolean needsExecutable = getProperties(fileName).getPropertyValue(SVNProperty.EXECUTABLE) != null;
                if (needsReadonly) {
                    SVNFileUtil.setReadonly(wcFile, true);
                    overwritten = true;
                }
                if (needsExecutable) {
                    SVNFileUtil.setExecutable(wcFile, true);
                    overwritten = true;
                }
                if (fileType == SVNFileType.FILE) {
                    SVNFileUtil.rename(tmpFile, baseFile);
                }
                if (setReadWrite) {
                    SVNFileUtil.setReadonly(wcFile, false);
                    overwritten = true;
                }
                if (setNotExecutable) {
                    SVNFileUtil.setExecutable(wcFile, false);
                    overwritten = true;
                }
                if (overwritten) {
                    textTime = SVNFileUtil.getFileLastModified(wcFile);
                    fileLength = SVNFileUtil.getFileLength(wcFile);
                }
            } catch (SVNException svne) {
                SVNErrorMessage err = SVNErrorMessage.create(errorCode, "Error replacing text-base of ''{0}''", fileName);
                SVNErrorManager.error(err, svne, SVNLogType.WC);
            } finally {
                tmpFile2.delete();
                tmpFile.delete();
            }
        }

        // update entry
        Map entryAttrs = new SVNHashMap();
        entryAttrs.put(SVNProperty.REVISION, SVNProperty.toString(revisionNumber));
        entryAttrs.put(SVNProperty.KIND, getThisDirName().equals(fileName) ? SVNProperty.KIND_DIR : SVNProperty.KIND_FILE);
        if (!implicit) {
            entryAttrs.put(SVNProperty.SCHEDULE, null);
        }
        entryAttrs.put(SVNProperty.COPIED, SVNProperty.toString(false));
        entryAttrs.put(SVNProperty.DELETED, SVNProperty.toString(false));
        if (textTime != 0 && !implicit) {
            entryAttrs.put(SVNProperty.TEXT_TIME, SVNDate.formatDate(new Date(textTime)));
        }
        entryAttrs.put(SVNProperty.CONFLICT_NEW, null);
        entryAttrs.put(SVNProperty.CONFLICT_OLD, null);
        entryAttrs.put(SVNProperty.CONFLICT_WRK, null);
        entryAttrs.put(SVNProperty.PROP_REJECT_FILE, null);
        entryAttrs.put(SVNProperty.COPYFROM_REVISION, null);
        entryAttrs.put(SVNProperty.COPYFROM_URL, null);
        entryAttrs.put(SVNProperty.HAS_PROP_MODS, SVNProperty.toString(false));
        entryAttrs.put(SVNProperty.WORKING_SIZE, Long.toString(fileLength));


        try {
            modifyEntry(fileName, entryAttrs, false, true);
        } catch (SVNException svne) {
            SVNErrorMessage err = SVNErrorMessage.create(errorCode, "Error modifying entry of ''{0}''", fileName);
            SVNErrorManager.error(err, svne, SVNLogType.WC);
        }
        SVNFileUtil.deleteFile(wcPropsFile);

        if (!getThisDirName().equals(fileName)) {
            return;
        }
        // update entry in parent.
        File dirFile = getRoot();
        if (getWCAccess().isWCRoot(getRoot())) {
            return;
        }

        boolean unassociated = false;
        SVNAdminArea parentArea = null;
        try {
            parentArea = getWCAccess().retrieve(dirFile.getParentFile());
        } catch (SVNException svne) {
            if (svne.getErrorMessage().getErrorCode() == SVNErrorCode.WC_NOT_LOCKED) {
                parentArea = getWCAccess().open(dirFile.getParentFile(), true, false, 0);
                unassociated = true;
            } else {
                throw svne;
            }
        }

        SVNEntry entryInParent = parentArea.getEntry(dirFile.getName(), false);
        if (entryInParent != null) {
            entryAttrs.clear();

            if (!implicit) {
                entryAttrs.put(SVNProperty.SCHEDULE, null);
            }
            entryAttrs.put(SVNProperty.COPIED, SVNProperty.toString(false));
            entryAttrs.put(SVNProperty.COPYFROM_REVISION, null);
            entryAttrs.put(SVNProperty.COPYFROM_URL, null);
            entryAttrs.put(SVNProperty.DELETED, SVNProperty.toString(false));
            try {
                parentArea.modifyEntry(entryInParent.getName(), entryAttrs, true, true);
            } catch (SVNException svne) {
                SVNErrorMessage err = SVNErrorMessage.create(errorCode, "Error modifying entry of ''{0}''", fileName);
                SVNErrorManager.error(err, svne, SVNLogType.WC);
            }
        }
        parentArea.saveEntries(false);

        if (unassociated) {
            getWCAccess().closeAdminArea(dirFile.getParentFile());
        }
    }

    public boolean unlock() throws SVNException {
        if (!myLockFile.exists()) {
            return true;
        }
        // only if there are not locks or killme files.
        boolean killMe = getAdminFile(ADM_KILLME).exists();
        if (killMe) {
            return false;
        }
        File[] logs = SVNFileListUtil.listFiles(getAdminDirectory());
        for (int i = 0; logs != null && i < logs.length; i++) {
            File log = logs[i];
            if ("log".equals(log.getName()) || log.getName().startsWith("log.")) {
                if (log.isFile() && log.exists()) {
                    SVNDebugLog.getDefaultLog().logFiner(SVNLogType.WC, "unlock: log file: '" + log.getName() + "', listed, and exists.");
                    return false;
                }
                SVNDebugLog.getDefaultLog().logFiner(SVNLogType.WC, "unlock: log file: '" + log.getName() + "', listed, but does not exist.");
            }
        }
        boolean deleted = SVNFileUtil.deleteFile(myLockFile);
        if (!deleted) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_LOCKED, "Failed to unlock working copy ''{0}''", getRoot());
            SVNErrorManager.error(err, SVNLogType.WC);
        }
        return deleted;
    }

    public boolean isVersioned() {
        if (getAdminDirectory().isDirectory() && myEntriesFile.canRead()) {
            try {
                if (getEntry("", false) != null) {
                    return true;
                }
            } catch (SVNException e) {
                //
            }
        }
        return false;
    }

    public boolean isLocked() throws SVNException {
        if (!myWasLocked) {
            return false;
        }
        SVNFileType type = SVNFileType.getType(myLockFile);
        if (type == SVNFileType.FILE) {
            return true;
        } else if (type == SVNFileType.NONE) {
            return false;
        }

        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_LOCKED, "Lock file ''{0}'' is not a regular file", myLockFile);
        SVNErrorManager.error(err, SVNLogType.WC);
        return false;
    }

    public boolean hasTreeConflict(String name) throws SVNException {
        return false;
    }

    public SVNTreeConflictDescription getTreeConflict(String name) throws SVNException {
        return null;
    }

    public void addTreeConflict(SVNTreeConflictDescription conflict) throws SVNException {
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                "This feature is not supported in version {0} of working copy format", String.valueOf(getFormatVersion()));
        SVNErrorManager.error(err, SVNLogType.WC);
    }

    public SVNTreeConflictDescription deleteTreeConflict(String name) throws SVNException {
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                "This feature is not supported in version {0} of working copy format", String.valueOf(getFormatVersion()));
        SVNErrorManager.error(err, SVNLogType.WC);
        return null;
    }

    public void setFileExternalLocation(String name, SVNURL url, SVNRevision pegRevision, SVNRevision revision, SVNURL reposRootURL) throws SVNException {
        SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNSUPPORTED_FEATURE,
                "This feature is not supported in version {0} of working copy format", String.valueOf(getFormatVersion()));
        SVNErrorManager.error(err, SVNLogType.WC);
    }

    public int getFormatVersion() {
        return WC_FORMAT;
    }

    protected SVNAdminArea createAdminAreaForDir(File dir) {
        return new SVNAdminArea14(dir);
    }

    protected boolean isEntryPropertyApplicable(String propName) {
        return propName != null && !INAPPLICABLE_PROPERTIES.contains(propName);
    }

}
