/*
 * 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 liquibase.util.file;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Stack;

/**
 *
Original code copied from Commons IO.
 * Some modifications done. Don't update directly to new versions from commons io.
 *

 * General filename and filepath manipulation utilities.
 *

 * When dealing with filenames you can hit problems when moving from a Windows
 * based development machine to a Unix based production machine.
 * This class aims to help avoid those problems.
 *

 * NOTE: You may be able to avoid using this class entirely simply by
 * using JDK {@link java.io.File File} objects and the two argument constructor
 * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}.
 *

 * Most methods on this class are designed to work the same on both Unix and Windows.
 * Those that don't include 'System', 'Unix' or 'Windows' in their name.
 *

 * Most methods recognise both separators (forward and back), and both
 * sets of prefixes. See the javadoc of each method for details.
 *

 * This class defines six components within a filename
 * (example C:\dev\project\file.txt):
 *

 *
the prefix - C:\

 *
the path - dev\project\

 *
the full path - C:\dev\project\

 *
the name - file.txt

 *
the base name - file

 *
the extension - txt

 *

 * Note that this class works best if directory filenames end with a separator.
 * If you omit the last separator, it is impossible to determine if the filename
 * corresponds to a file or a directory. As a result, we have chosen to say
 * it corresponds to a file.
 *

 * This class only supports Unix and Windows style names.
 * Prefixes are matched as follows:
 *

 * Windows:
 * a\b\c.txt           --> ""          --> relative
 * \a\b\c.txt          --> "\"         --> current drive absolute
 * C:a\b\c.txt         --> "C:"        --> drive relative
 * C:\a\b\c.txt        --> "C:\"       --> absolute
 * \\server\a\b\c.txt  --> "\\server\" --> UNC
 *
 * Unix:
 * a/b/c.txt           --> ""          --> relative
 * /a/b/c.txt          --> "/"         --> absolute
 * ~/a/b/c.txt         --> "~/"        --> current user
 * ~                   --> "~/"        --> current user (slash added)
 * ~user/a/b/c.txt     --> "~user/"    --> named user
 * ~user               --> "~user/"    --> named user (slash added)
 *

 * Both prefix styles are matched always, irrespective of the machine that you are
 * currently running on.
 *

 * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils.
 *
 * @author Kevin A. Burton
 * @author Scott Sanders
 * @author Daniel Rall
 * @author Christoph.Reck
 * @author Peter Donald
 * @author Jeff Turner
 * @author Matthew Hawthorne
 * @author Martin Cooper
 * @author Jeremias Maerki
 * @author Stephen Colebourne
 * @version $Id: FilenameUtils.java 609870 2008-01-08 04:46:26Z niallp $
 * @since Commons IO 1.1
 */
public class FilenameUtils {

    /**
     * The extension separator character.
     * @since Commons IO 1.4
     */
    public static final char EXTENSION_SEPARATOR = '.';

    /**
     * The extension separator String.
     * @since Commons IO 1.4
     */
    public static final String EXTENSION_SEPARATOR_STR = Character.valueOf(EXTENSION_SEPARATOR).toString();

    /**
     * The Unix separator character.
     */
    private static final char UNIX_SEPARATOR = '/';

    /**
     * The Windows separator character.
     * Changed from original commons io implementation:
     *   We have to use '/' because we use to resolve classpath paths that only works with '/'
     */
    private static final char WINDOWS_SEPARATOR = '/';

    /**
     * The system separator character.
     * Changed to '/' from the same reason as WINDOWS_SEPARATOR
     */
    private static final char SYSTEM_SEPARATOR = '/';

    /**
     * The separator character that is the opposite of the system separator.
     */
    private static final char OTHER_SEPARATOR;
    static {
        if (isSystemWindows()) {
            OTHER_SEPARATOR = UNIX_SEPARATOR;
        } else {
            OTHER_SEPARATOR = WINDOWS_SEPARATOR;
        }
    }

    /**
     * Instances should NOT be constructed in standard programming.
     */
    public FilenameUtils() {
        super();
    }

    //-----------------------------------------------------------------------
    /**
     * Determines if Windows file system is in use.
     *
     * @return true if the system is Windows
     */
    static boolean isSystemWindows() {
        return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR;
    }

    //-----------------------------------------------------------------------
    /**
     * Checks if the character is a separator.
     *
     * @param ch  the character to check
     * @return true if it is a separator character
     */
    private static boolean isSeparator(char ch) {
        return ch == '/'; //java uses this as a separator in both windows and unix
    }

    //-----------------------------------------------------------------------
    /**
     * Normalizes a path, removing double and single dot path steps.
     *

     * This method normalizes a path to a standard format.
     * The input may contain separators in either Unix or Windows format.
     * The output will contain separators in the format of the system.
     *

     * A trailing slash will be retained.
     * A double slash will be merged to a single slash (but UNC names are handled).
     * A single dot path segment will be removed.
     * A double dot will cause that path segment and the one before to be removed.
     * If the double dot has no parent path segment to work with, null
     * is returned.
     *

     * The output will be the same on both Unix and Windows except
     * for the separator character.
     *

     * /foo//               -->   /foo/
     * /foo/./              -->   /foo/
     * /foo/../bar          -->   /bar
     * /foo/../bar/         -->   /bar/
     * /foo/../bar/../baz   -->   /baz
     * //foo//./bar         -->   /foo/bar
     * /../                 -->   null
     * ../foo               -->   null
     * foo/bar/..           -->   foo/
     * foo/../../bar        -->   null
     * foo/../bar           -->   bar
     * //server/foo/../bar  -->   //server/bar
     * //server/../bar      -->   null
     * C:\foo\..\bar        -->   C:\bar
     * C:\..\bar            -->   null
     * ~/foo/../bar/        -->   ~/bar/
     * ~/../bar             -->   null
     *

     * (Note the file separator returned will be correct for Windows/Unix)
     *
     * @param filename  the filename to normalize, null returns null
     * @return the normalized filename, or null if invalid
     */
    public static String normalize(String filename) {
        return doNormalize(filename, true);
    }

    //-----------------------------------------------------------------------
    /**
     * Normalizes a path, removing double and single dot path steps,
     * and removing any final directory separator.
     *

     * This method normalizes a path to a standard format.
     * The input may contain separators in either Unix or Windows format.
     * The output will contain separators in the format of the system.
     *

     * A trailing slash will be removed.
     * A double slash will be merged to a single slash (but UNC names are handled).
     * A single dot path segment will be removed.
     * A double dot will cause that path segment and the one before to be removed.
     * If the double dot has no parent path segment to work with, null
     * is returned.
     *

     * The output will be the same on both Unix and Windows except
     * for the separator character.
     *

     * /foo//               -->   /foo
     * /foo/./              -->   /foo
     * /foo/../bar          -->   /bar
     * /foo/../bar/         -->   /bar
     * /foo/../bar/../baz   -->   /baz
     * //foo//./bar         -->   /foo/bar
     * /../                 -->   null
     * ../foo               -->   null
     * foo/bar/..           -->   foo
     * foo/../../bar        -->   null
     * foo/../bar           -->   bar
     * //server/foo/../bar  -->   //server/bar
     * //server/../bar      -->   null
     * C:\foo\..\bar        -->   C:\bar
     * C:\..\bar            -->   null
     * ~/foo/../bar/        -->   ~/bar
     * ~/../bar             -->   null
     *

     * (Note the file separator returned will be correct for Windows/Unix)
     *
     * @param filename  the filename to normalize, null returns null
     * @return the normalized filename, or null if invalid
     */
    public static String normalizeNoEndSeparator(String filename) {
        return doNormalize(filename, false);
    }

    /**
     * Internal method to perform the normalization.
     *
     * @param filename  the filename
     * @param keepSeparator  true to keep the final separator
     * @return the normalized filename
     */
    private static String doNormalize(String filename, boolean keepSeparator) {
        if (filename == null) {
            return null;
        }
        int size = filename.length();
        if (size == 0) {
            return filename;
        }
        int prefix = getPrefixLength(filename);
        if (prefix < 0) {
            return null;
        }

        char[] array = new char[size + 2];  // +1 for possible extra slash, +2 for arraycopy
        filename.getChars(0, filename.length(), array, 0);

        // fix separators throughout
        for (int i = 0; i < array.length; i++) {
            if (array[i] == OTHER_SEPARATOR) {
                array[i] = SYSTEM_SEPARATOR;
            }
        }

        // add extra separator on the end to simplify code below
        boolean lastIsDirectory = true;
        if (array[size - 1] != SYSTEM_SEPARATOR) {
            array[size++] = SYSTEM_SEPARATOR;
            lastIsDirectory = false;
        }

        // adjoining slashes
        for (int i = prefix + 1; i < size; i++) {
            if ((array[i] == SYSTEM_SEPARATOR) && (array[i - 1] == SYSTEM_SEPARATOR)) {
                System.arraycopy(array, i, array, i - 1, size - i);
                size--;
                i--;
            }
        }

        // dot slash
        for (int i = prefix + 1; i < size; i++) {
            if ((array[i] == SYSTEM_SEPARATOR) && (array[i - 1] == '.') && ((i == (prefix + 1)) || (array[i - 2] ==
                SYSTEM_SEPARATOR))) {
                if (i == (size - 1)) {
                    lastIsDirectory = true;
                }
                System.arraycopy(array, i + 1, array, i - 1, size - i);
                size -=2;
                i--;
            }
        }

        // double dot slash
        outer:
        for (int i = prefix + 2; i < size; i++) {
            if ((array[i] == SYSTEM_SEPARATOR) && (array[i - 1] == '.') && (array[i - 2] == '.') && ((i == (prefix +
                2)) || (array[i - 3] == SYSTEM_SEPARATOR))) {
                if (i == (prefix + 2)) {
                    return null;
                }
                if (i == (size - 1)) {
                    lastIsDirectory = true;
                }
                int j;
                for (j = i - 4 ; j >= prefix; j--) {
                    if (array[j] == SYSTEM_SEPARATOR) {
                        // remove b/../ from a/b/../c
                        System.arraycopy(array, i + 1, array, j + 1, size - i);
                        size -= (i - j);
                        i = j + 1;
                        continue outer;
                    }
                }
                // remove a/../ from a/../c
                System.arraycopy(array, i + 1, array, prefix, size - i);
                size -= ((i + 1) - prefix);
                i = prefix + 1;
            }
        }

        if (size <= 0) {  // should never be less than 0
            return "";
        }
        if (size <= prefix) {  // should never be less than prefix
            return new String(array, 0, size);
        }
        if (lastIsDirectory && keepSeparator) {
            return new String(array, 0, size);  // keep trailing separator
        }
        return new String(array, 0, size - 1);  // lose trailing separator
    }

    //-----------------------------------------------------------------------
    /**
     * Concatenates a filename to a base path using normal command line style rules.
     *

     * The effect is equivalent to resultant directory after changing
     * directory to the first argument, followed by changing directory to
     * the second argument.
     *

     * The first argument is the base path, the second is the path to concatenate.
     * The returned path is always normalized via {@link #normalize(String)},
     * thus .. is handled.
     *

     * If pathToAdd is absolute (has an absolute prefix), then
     * it will be normalized and returned.
     * Otherwise, the paths will be joined, normalized and returned.
     *

     * The output will be the same on both Unix and Windows except
     * for the separator character.
     *

     * /foo/ + bar          -->   /foo/bar
     * /foo + bar           -->   /foo/bar
     * /foo + /bar          -->   /bar
     * /foo + C:/bar        -->   C:/bar
     * /foo + C:bar         -->   C:bar (*)
     * /foo/a/ + ../bar     -->   foo/bar
     * /foo/ + ../../bar    -->   null
     * /foo/ + /bar         -->   /bar
     * /foo/.. + /bar       -->   /bar
     * /foo + bar/c.txt     -->   /foo/bar/c.txt
     * /foo/c.txt + bar     -->   /foo/c.txt/bar (!)
     *

     * (*) Note that the Windows relative drive prefix is unreliable when
     * used with this method.
     * (!) Note that the first parameter must be a path. If it ends with a name, then
     * the name will be built into the concatenated path. If this might be a problem,
     * use {@link #getFullPath(String)} on the base path argument.
     *
     * @param basePath  the base path to attach to, always treated as a path
     * @param fullFilenameToAdd  the filename (or path) to attach to the base
     * @return the concatenated path, or null if invalid
     */
    public static String concat(String basePath, String fullFilenameToAdd) {
        int prefix = getPrefixLength(fullFilenameToAdd);
        if (prefix < 0) {
            return null;
        }
        if (prefix > 0) {
            return normalize(fullFilenameToAdd);
        }
        if (basePath == null) {
            return fullFilenameToAdd;
        }
        int len = basePath.length();
        if (len == 0) {
            return normalize(fullFilenameToAdd);
        }
        char ch = basePath.charAt(len - 1);
        if (isSeparator(ch)) {
            return normalize(basePath + fullFilenameToAdd);
        } else {
            return normalize(basePath + '/' + fullFilenameToAdd);
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Converts all separators to the Unix separator of forward slash.
     *
     * @param path  the path to be changed, null ignored
     * @return the updated path
     */
    public static String separatorsToUnix(String path) {
        if ((path == null) || (path.indexOf(WINDOWS_SEPARATOR) == -1)) {
            return path;
        }
        return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR);
    }

    /**
     * Converts all separators to the Windows separator of backslash.
     *
     * @param path  the path to be changed, null ignored
     * @return the updated path
     */
    public static String separatorsToWindows(String path) {
        if ((path == null) || (path.indexOf(UNIX_SEPARATOR) == -1)) {
            return path;
        }
        return path.replace(UNIX_SEPARATOR, WINDOWS_SEPARATOR);
    }

    /**
     * Converts all separators to the system separator.
     *
     * @param path  the path to be changed, null ignored
     * @return the updated path
     */
    public static String separatorsToSystem(String path) {
        if (path == null) {
            return null;
        }
        if (isSystemWindows()) {
            return separatorsToWindows(path);
        } else {
            return separatorsToUnix(path);
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Returns the length of the filename prefix, such as C:/ or ~/.
     *

     * This method will handle a file in either Unix or Windows format.
     *

     * The prefix length includes the first slash in the full filename
     * if applicable. Thus, it is possible that the length returned is greater
     * than the length of the input string.
     *

     * Windows:
     * a\b\c.txt           --> ""          --> relative
     * \a\b\c.txt          --> "\"         --> current drive absolute
     * C:a\b\c.txt         --> "C:"        --> drive relative
     * C:\a\b\c.txt        --> "C:\"       --> absolute
     * \\server\a\b\c.txt  --> "\\server\" --> UNC
     *
     * Unix:
     * a/b/c.txt           --> ""          --> relative
     * /a/b/c.txt          --> "/"         --> absolute
     * ~/a/b/c.txt         --> "~/"        --> current user
     * ~                   --> "~/"        --> current user (slash added)
     * ~user/a/b/c.txt     --> "~user/"    --> named user
     * ~user               --> "~user/"    --> named user (slash added)
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     * ie. both Unix and Windows prefixes are matched regardless.
     *
     * @param filename  the filename to find the prefix in, null returns -1
     * @return the length of the prefix, -1 if invalid or null
     */
    public static int getPrefixLength(String filename) {
        if (filename == null) {
            return -1;
        }
        int len = filename.length();
        if (len == 0) {
            return 0;
        }
        char ch0 = filename.charAt(0);
        if (ch0 == ':') {
            return -1;
        }
        if (len == 1) {
            if (ch0 == '~') {
                return 2;  // return a length greater than the input
            }
            return (isSeparator(ch0) ? 1 : 0);
        } else {
            if (ch0 == '~') {
                int posUnix = filename.indexOf(UNIX_SEPARATOR, 1);
                int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1);
                if ((posUnix == -1) && (posWin == -1)) {
                    return len + 1;  // return a length greater than the input
                }
                posUnix = ((posUnix == -1) ? posWin : posUnix);
                posWin = ((posWin == -1) ? posUnix : posWin);
                return Math.min(posUnix, posWin) + 1;
            }
            char ch1 = filename.charAt(1);
            if (ch1 == ':') {
                ch0 = Character.toUpperCase(ch0);
                if ((ch0 >= 'A') && (ch0 <= 'Z')) {
                    if ((len == 2) || (isSeparator(filename.charAt(2)) == false)) {
                        return 2;
                    }
                    return 3;
                }
                return -1;

            } else if (isSeparator(ch0) && isSeparator(ch1)) {
                int posUnix = filename.indexOf(UNIX_SEPARATOR, 2);
                int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2);
                if (((posUnix == -1) && (posWin == -1)) || (posUnix == 2) || (posWin == 2)) {
                    return -1;
                }
                posUnix = ((posUnix == -1) ? posWin : posUnix);
                posWin = ((posWin == -1) ? posUnix : posWin);
                return Math.min(posUnix, posWin) + 1;
            } else {
                return (isSeparator(ch0) ? 1 : 0);
            }
        }
    }

    /**
     * Returns the index of the last directory separator character.
     *

     * This method will handle a file in either Unix or Windows format.
     * The position of the last forward or backslash is returned.
     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename  the filename to find the last path separator in, null returns -1
     * @return the index of the last separator character, or -1 if there
     * is no such character
     */
    public static int indexOfLastSeparator(String filename) {
        if (filename == null) {
            return -1;
        }
        int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR);
        int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR);
        return Math.max(lastUnixPos, lastWindowsPos);
    }

    /**
     * Returns the index of the last extension separator character, which is a dot.
     *

     * This method also checks that there is no directory separator after the last dot.
     * To do this it uses {@link #indexOfLastSeparator(String)} which will
     * handle a file in either Unix or Windows format.
     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename  the filename to find the last path separator in, null returns -1
     * @return the index of the last separator character, or -1 if there
     * is no such character
     */
    public static int indexOfExtension(String filename) {
        if (filename == null) {
            return -1;
        }
        int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
        int lastSeparator = indexOfLastSeparator(filename);
        return ((lastSeparator > extensionPos) ? -1 : extensionPos);
    }

    //-----------------------------------------------------------------------
    /**
     * Gets the prefix from a full filename, such as C:/
     * or ~/.
     *

     * This method will handle a file in either Unix or Windows format.
     * The prefix includes the first slash in the full filename where applicable.
     *

     * Windows:
     * a\b\c.txt           --> ""          --> relative
     * \a\b\c.txt          --> "\"         --> current drive absolute
     * C:a\b\c.txt         --> "C:"        --> drive relative
     * C:\a\b\c.txt        --> "C:\"       --> absolute
     * \\server\a\b\c.txt  --> "\\server\" --> UNC
     *
     * Unix:
     * a/b/c.txt           --> ""          --> relative
     * /a/b/c.txt          --> "/"         --> absolute
     * ~/a/b/c.txt         --> "~/"        --> current user
     * ~                   --> "~/"        --> current user (slash added)
     * ~user/a/b/c.txt     --> "~user/"    --> named user
     * ~user               --> "~user/"    --> named user (slash added)
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     * ie. both Unix and Windows prefixes are matched regardless.
     *
     * @param filename  the filename to query, null returns null
     * @return the prefix of the file, null if invalid
     */
    public static String getPrefix(String filename) {
        if (filename == null) {
            return null;
        }
        int len = getPrefixLength(filename);
        if (len < 0) {
            return null;
        }
        if (len > filename.length()) {
            return filename + UNIX_SEPARATOR;  // we know this only happens for unix
        }
        return filename.substring(0, len);
    }

    /**
     * Gets the path from a full filename, which excludes the prefix.
     *

     * This method will handle a file in either Unix or Windows format.
     * The method is entirely text based, and returns the text before and
     * including the last forward or backslash.
     *

     * C:\a\b\c.txt --> a\b\
     * ~/a/b/c.txt  --> a/b/
     * a.txt        --> ""
     * a/b/c        --> a/b/
     * a/b/c/       --> a/b/c/
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *

     * This method drops the prefix from the result.
     * See {@link #getFullPath(String)} for the method that retains the prefix.
     *
     * @param filename  the filename to query, null returns null
     * @return the path of the file, an empty string if none exists, null if invalid
     */
    public static String getPath(String filename) {
        return doGetPath(filename, 1);
    }

    /**
     * Gets the path from a full filename, which excludes the prefix, and
     * also excluding the final directory separator.
     *

     * This method will handle a file in either Unix or Windows format.
     * The method is entirely text based, and returns the text before the
     * last forward or backslash.
     *

     * C:\a\b\c.txt --> a\b
     * ~/a/b/c.txt  --> a/b
     * a.txt        --> ""
     * a/b/c        --> a/b
     * a/b/c/       --> a/b/c
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *

     * This method drops the prefix from the result.
     * See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix.
     *
     * @param filename  the filename to query, null returns null
     * @return the path of the file, an empty string if none exists, null if invalid
     */
    public static String getPathNoEndSeparator(String filename) {
        return doGetPath(filename, 0);
    }

    /**
     * Does the work of getting the path.
     *
     * @param filename  the filename
     * @param separatorAdd  0 to omit the end separator, 1 to return it
     * @return the path
     */
    private static String doGetPath(String filename, int separatorAdd) {
        if (filename == null) {
            return null;
        }
        int prefix = getPrefixLength(filename);
        if (prefix < 0) {
            return null;
        }
        int index = indexOfLastSeparator(filename);
        if ((prefix >= filename.length()) || (index < 0)) {
            return "";
        }
        return filename.substring(prefix, index + separatorAdd);
    }

    /**
     * Gets the full path from a full filename, which is the prefix + path.
     *

     * This method will handle a file in either Unix or Windows format.
     * The method is entirely text based, and returns the text before and
     * including the last forward or backslash.
     *

     * C:\a\b\c.txt --> C:\a\b\
     * ~/a/b/c.txt  --> ~/a/b/
     * a.txt        --> ""
     * a/b/c        --> a/b/
     * a/b/c/       --> a/b/c/
     * C:           --> C:
     * C:\          --> C:\
     * ~            --> ~/
     * ~/           --> ~/
     * ~user        --> ~user/
     * ~user/       --> ~user/
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename  the filename to query, null returns null
     * @return the path of the file, an empty string if none exists, null if invalid
     */
    public static String getFullPath(String filename) {
        return doGetFullPath(filename, true);
    }

    /**
     * Gets the full path from a full filename, which is the prefix + path,
     * and also excluding the final directory separator.
     *

     * This method will handle a file in either Unix or Windows format.
     * The method is entirely text based, and returns the text before the
     * last forward or backslash.
     *

     * C:\a\b\c.txt --> C:\a\b
     * ~/a/b/c.txt  --> ~/a/b
     * a.txt        --> ""
     * a/b/c        --> a/b
     * a/b/c/       --> a/b/c
     * C:           --> C:
     * C:\          --> C:\
     * ~            --> ~
     * ~/           --> ~
     * ~user        --> ~user
     * ~user/       --> ~user
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename  the filename to query, null returns null
     * @return the path of the file, an empty string if none exists, null if invalid
     */
    public static String getFullPathNoEndSeparator(String filename) {
        return doGetFullPath(filename, false);
    }

    /**
     * Does the work of getting the path.
     *
     * @param filename  the filename
     * @param includeSeparator  true to include the end separator
     * @return the path
     */
    private static String doGetFullPath(String filename, boolean includeSeparator) {
        if (filename == null) {
            return null;
        }
        int prefix = getPrefixLength(filename);
        if (prefix < 0) {
            return null;
        }
        if (prefix >= filename.length()) {
            if (includeSeparator) {
                return getPrefix(filename);  // add end slash if necessary
            } else {
                return filename;
            }
        }
        int index = indexOfLastSeparator(filename);
        if (index < 0) {
            return filename.substring(0, prefix);
        }
        int end = index + (includeSeparator ?  1 : 0);
        return filename.substring(0, end);
    }

    /**
     * Gets the name minus the path from a full filename.
     *

     * This method will handle a file in either Unix or Windows format.
     * The text after the last forward or backslash is returned.
     *

     * a/b/c.txt --> c.txt
     * a.txt     --> a.txt
     * a/b/c     --> c
     * a/b/c/    --> ""
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename  the filename to query, null returns null
     * @return the name of the file without the path, or an empty string if none exists
     */
    public static String getName(String filename) {
        if (filename == null) {
            return null;
        }
        int index = indexOfLastSeparator(filename);
        return filename.substring(index + 1);
    }

    /**
     * Gets the base name, minus the full path and extension, from a full filename.
     *

     * This method will handle a file in either Unix or Windows format.
     * The text after the last forward or backslash and before the last dot is returned.
     *

     * a/b/c.txt --> c
     * a.txt     --> a
     * a/b/c     --> c
     * a/b/c/    --> ""
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename  the filename to query, null returns null
     * @return the name of the file without the path, or an empty string if none exists
     */
    public static String getBaseName(String filename) {
        return removeExtension(getName(filename));
    }

    /**
     * Gets the extension of a filename.
     *

     * This method returns the textual part of the filename after the last dot.
     * There must be no directory separator after the dot.
     *

     * foo.txt      --> "txt"
     * a/b/c.jpg    --> "jpg"
     * a/b.txt/c    --> ""
     * a/b/c        --> ""
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename the filename to retrieve the extension of.
     * @return the extension of the file or an empty string if none exists.
     */
    public static String getExtension(String filename) {
        if (filename == null) {
            return null;
        }
        int index = indexOfExtension(filename);
        if (index == -1) {
            return "";
        } else {
            return filename.substring(index + 1);
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Removes the extension from a filename.
     *

     * This method returns the textual part of the filename before the last dot.
     * There must be no directory separator after the dot.
     *

     * foo.txt    --> foo
     * a\b\c.jpg  --> a\b\c
     * a\b\c      --> a\b\c
     * a.b\c      --> a.b\c
     *

     *

     * The output will be the same irrespective of the machine that the code is running on.
     *
     * @param filename  the filename to query, null returns null
     * @return the filename minus the extension
     */
    public static String removeExtension(String filename) {
        if (filename == null) {
            return null;
        }
        int index = indexOfExtension(filename);
        if (index == -1) {
            return filename;
        } else {
            return filename.substring(0, index);
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Checks whether two filenames are equal exactly.
     *

     * No processing is performed on the filenames other than comparison,
     * thus this is merely a null-safe case-sensitive equals.
     *
     * @param filename1  the first filename to query, may be null
     * @param filename2  the second filename to query, may be null
     * @return true if the filenames are equal, null equals null
     * @see IOCase#SENSITIVE
     */
    public static boolean equals(String filename1, String filename2) {
        return equals(filename1, filename2, false, IOCase.SENSITIVE);
    }

    /**
     * Checks whether two filenames are equal using the case rules of the system.
     *

     * No processing is performed on the filenames other than comparison.
     * The check is case-sensitive on Unix and case-insensitive on Windows.
     *
     * @param filename1  the first filename to query, may be null
     * @param filename2  the second filename to query, may be null
     * @return true if the filenames are equal, null equals null
     * @see IOCase#SYSTEM
     */
    public static boolean equalsOnSystem(String filename1, String filename2) {
        return equals(filename1, filename2, false, IOCase.SYSTEM);
    }

    //-----------------------------------------------------------------------
    /**
     * Checks whether two filenames are equal after both have been normalized.
     *

     * Both filenames are first passed to {@link #normalize(String)}.
     * The check is then performed in a case-sensitive manner.
     *
     * @param filename1  the first filename to query, may be null
     * @param filename2  the second filename to query, may be null
     * @return true if the filenames are equal, null equals null
     * @see IOCase#SENSITIVE
     */
    public static boolean equalsNormalized(String filename1, String filename2) {
        return equals(filename1, filename2, true, IOCase.SENSITIVE);
    }

    /**
     * Checks whether two filenames are equal after both have been normalized
     * and using the case rules of the system.
     *

     * Both filenames are first passed to {@link #normalize(String)}.
     * The check is then performed case-sensitive on Unix and
     * case-insensitive on Windows.
     *
     * @param filename1  the first filename to query, may be null
     * @param filename2  the second filename to query, may be null
     * @return true if the filenames are equal, null equals null
     * @see IOCase#SYSTEM
     */
    public static boolean equalsNormalizedOnSystem(String filename1, String filename2) {
        return equals(filename1, filename2, true, IOCase.SYSTEM);
    }

    /**
     * Checks whether two filenames are equal, optionally normalizing and providing
     * control over the case-sensitivity.
     *
     * @param filename1  the first filename to query, may be null
     * @param filename2  the second filename to query, may be null
     * @param normalized  whether to normalize the filenames
     * @param caseSensitivity  what case sensitivity rule to use, null means case-sensitive
     * @return true if the filenames are equal, null equals null
     * @since Commons IO 1.3
     */
    public static boolean equals(
            String filename1, String filename2,
            boolean normalized, IOCase caseSensitivity) {

        if ((filename1 == null) && (filename2 == null)) {
            return true;
        } else if ((filename1 == null) || (filename2 == null)) {
            return false;
        }

        if (normalized) {
            filename1 = normalize(filename1);
            filename2 = normalize(filename2);
            if ((filename1 == null) || (filename2 == null)) {
                throw new NullPointerException(
                    "Error normalizing one or both of the file names");
            }
        }
        if (caseSensitivity == null) {
            caseSensitivity = IOCase.SENSITIVE;
        }
        return caseSensitivity.checkEquals(filename1, filename2);
    }

    //-----------------------------------------------------------------------
    /**
     * Checks whether the extension of the filename is that specified.
     *

     * This method obtains the extension as the textual part of the filename
     * after the last dot. There must be no directory separator after the dot.
     * The extension check is case-sensitive on all platforms.
     *
     * @param filename  the filename to query, null returns false
     * @param extension  the extension to check for, null or empty checks for no extension
     * @return true if the filename has the specified extension
     */
    public static boolean isExtension(String filename, String extension) {
        if (filename == null) {
            return false;
        }
        if ((extension == null) || extension.isEmpty()) {
            return (indexOfExtension(filename) == -1);
        }
        String fileExt = getExtension(filename);
        return fileExt.equals(extension);
    }

    /**
     * Checks whether the extension of the filename is one of those specified.
     *

     * This method obtains the extension as the textual part of the filename
     * after the last dot. There must be no directory separator after the dot.
     * The extension check is case-sensitive on all platforms.
     *
     * @param filename  the filename to query, null returns false
     * @param extensions  the extensions to check for, null checks for no extension
     * @return true if the filename is one of the extensions
     */
    public static boolean isExtension(String filename, String[] extensions) {
        if (filename == null) {
            return false;
        }
        if ((extensions == null) || (extensions.length == 0)) {
            return (indexOfExtension(filename) == -1);
        }
        String fileExt = getExtension(filename);
        for (int i = 0; i < extensions.length; i++) {
            if (fileExt.equals(extensions[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks whether the extension of the filename is one of those specified.
     *

     * This method obtains the extension as the textual part of the filename
     * after the last dot. There must be no directory separator after the dot.
     * The extension check is case-sensitive on all platforms.
     *
     * @param filename  the filename to query, null returns false
     * @param extensions  the extensions to check for, null checks for no extension
     * @return true if the filename is one of the extensions
     */
    public static boolean isExtension(String filename, Collection extensions) {
        if (filename == null) {
            return false;
        }
        if ((extensions == null) || extensions.isEmpty()) {
            return (indexOfExtension(filename) == -1);
        }
        String fileExt = getExtension(filename);
        for (Iterator it = extensions.iterator(); it.hasNext();) {
            if (fileExt.equals(it.next())) {
                return true;
            }
        }
        return false;
    }

    //-----------------------------------------------------------------------
    /**
     * Checks a filename to see if it matches the specified wildcard matcher,
     * always testing case-sensitive.
     *

     * The wildcard matcher uses the characters '?' and '*' to represent a
     * single or multiple wildcard characters.
     * This is the same as often found on Dos/Unix command lines.
     * The check is case-sensitive always.
     *

     * wildcardMatch("c.txt", "*.txt")      --> true
     * wildcardMatch("c.txt", "*.jpg")      --> false
     * wildcardMatch("a/b/c.txt", "a/b/*")  --> true
     * wildcardMatch("c.txt", "*.???")      --> true
     * wildcardMatch("c.txt", "*.????")     --> false
     *

     *
     * @param filename  the filename to match on
     * @param wildcardMatcher  the wildcard string to match against
     * @return true if the filename matches the wilcard string
     * @see IOCase#SENSITIVE
     */
    public static boolean wildcardMatch(String filename, String wildcardMatcher) {
        return wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE);
    }

    /**
     * Checks a filename to see if it matches the specified wildcard matcher
     * using the case rules of the system.
     *

     * The wildcard matcher uses the characters '?' and '*' to represent a
     * single or multiple wildcard characters.
     * This is the same as often found on Dos/Unix command lines.
     * The check is case-sensitive on Unix and case-insensitive on Windows.
     *

     * wildcardMatch("c.txt", "*.txt")      --> true
     * wildcardMatch("c.txt", "*.jpg")      --> false
     * wildcardMatch("a/b/c.txt", "a/b/*")  --> true
     * wildcardMatch("c.txt", "*.???")      --> true
     * wildcardMatch("c.txt", "*.????")     --> false
     *

     *
     * @param filename  the filename to match on
     * @param wildcardMatcher  the wildcard string to match against
     * @return true if the filename matches the wilcard string
     * @see IOCase#SYSTEM
     */
    public static boolean wildcardMatchOnSystem(String filename, String wildcardMatcher) {
        return wildcardMatch(filename, wildcardMatcher, IOCase.SYSTEM);
    }

    /**
     * Checks a filename to see if it matches the specified wildcard matcher
     * allowing control over case-sensitivity.
     *

     * The wildcard matcher uses the characters '?' and '*' to represent a
     * single or multiple wildcard characters.
     *
     * @param filename  the filename to match on
     * @param wildcardMatcher  the wildcard string to match against
     * @param caseSensitivity  what case sensitivity rule to use, null means case-sensitive
     * @return true if the filename matches the wilcard string
     * @since Commons IO 1.3
     */
    public static boolean wildcardMatch(String filename, String wildcardMatcher, IOCase caseSensitivity) {
        if ((filename == null) && (wildcardMatcher == null)) {
            return true;
        }
        if ((filename == null) || (wildcardMatcher == null)) {
            return false;
        }
        if (caseSensitivity == null) {
            caseSensitivity = IOCase.SENSITIVE;
        }
        filename = caseSensitivity.convertCase(filename);
        wildcardMatcher = caseSensitivity.convertCase(wildcardMatcher);
        String[] wcs = splitOnTokens(wildcardMatcher);
        boolean anyChars = false;
        int textIdx = 0;
        int wcsIdx = 0;
        Stack backtrack = new Stack();

        // loop around a backtrack stack, to handle complex * matching
        do {
            if (!backtrack.isEmpty()) {
                int[] array = (int[]) backtrack.pop();
                wcsIdx = array[0];
                textIdx = array[1];
                anyChars = true;
            }

            // loop whilst tokens and text left to process
            label:
            while (wcsIdx < wcs.length) {

                switch (wcs[wcsIdx]) {
                    case "?":
                        // ? so move to next text char
                        textIdx++;
                        anyChars = false;

                        break;
                    case "*":
                        // set any chars status
                        anyChars = true;
                        if (wcsIdx == (wcs.length - 1)) {
                            textIdx = filename.length();
                        }

                        break;
                    default:
                        // matching text token
                        if (anyChars) {
                            // any chars then try to locate text token
                            textIdx = filename.indexOf(wcs[wcsIdx], textIdx);
                            if (textIdx == -1) {
                                // token not found
                                break label;
                            }
                            int repeat = filename.indexOf(wcs[wcsIdx], textIdx + 1);
                            if (repeat >= 0) {
                                backtrack.push(new int[]{wcsIdx, repeat});
                            }
                        } else {
                            // matching from current position
                            if (!filename.startsWith(wcs[wcsIdx], textIdx)) {
                                // couldnt match token
                                break label;
                            }
                        }

                        // matched text token, move text index to end of matched token
                        textIdx += wcs[wcsIdx].length();
                        anyChars = false;
                        break;
                }

                wcsIdx++;
            }

            // full match
            if ((wcsIdx == wcs.length) && (textIdx == filename.length())) {
                return true;
            }

        } while (!backtrack.isEmpty());

        return false;
    }

    /**
     * Splits a string into a number of tokens.
     *
     * @param text  the text to split
     * @return the tokens, never null
     */
    static String[] splitOnTokens(String text) {
        // used by wildcardMatch
        // package level so a unit test may run on this

        if ((text.indexOf("?") == -1) && (text.indexOf("*") == -1)) {
            return new String[] { text };
        }

        char[] array = text.toCharArray();
        ArrayList list = new ArrayList();
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < array.length; i++) {
            if ((array[i] == '?') || (array[i] == '*')) {
                if (buffer.length() != 0) {
                    list.add(buffer.toString());
                    buffer.setLength(0);
                }
                if (array[i] == '?') {
                    list.add("?");
                } else if (list.isEmpty() || ((i > 0) && ("*".equals(list.get(list.size() - 1)) == false))) {
                    list.add("*");
                }
            } else {
                buffer.append(array[i]);
            }
        }
        if (buffer.length() != 0) {
            list.add(buffer.toString());
        }

        return (String[]) list.toArray( new String[ list.size() ] );
    }

}


package liquibase.util.csv.opencsv.bean;

import liquibase.util.StringUtil;
import liquibase.util.csv.opencsv.CSVReader;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
 * Copyright 2007 Kyle Miller.
 *
 * Licensed 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.
 */

/**
 * Maps data to objects using the column names in the first row of the csv
 * file as reference.  This way the column order does not matter.
 *
 * @param <T>
 */

public class HeaderColumnNameMappingStrategy<T> implements MappingStrategy<T> {
   protected String[] header;
   protected Map<String, Integer> indexLookup = new HashMap<>();
   protected Map<String, PropertyDescriptor> descriptorMap;
   protected Map<String, BeanField> fieldMap;
   protected Class<T> type;
   protected boolean annotationDriven;
   protected boolean determinedIfAnnotationDriven;

   /**
    * Default constructor.
    */
   public HeaderColumnNameMappingStrategy() {
   }

   /**
    * Retrieves the header from the CSVReader.
    *
    * @param reader the CSVReader to use for header parsing
    * @throws IOException - thrown on error reading from the CSVReader.
    */
   @Override
   public void captureHeader(CSVReader reader) throws IOException {
      header = reader.readNext();
   }

   /**
    * Creates an index map of column names to column position.
    * @param values - array of header values.
    */
   protected void createIndexLookup(String[] values) {
      if (indexLookup.isEmpty()) {
         for (int i = 0; i < values.length; i++) {
            indexLookup.put(values[i], i);
         }
      }
   }

   /**
    * Resets index map of column names to column position.
    */
   protected void resetIndexMap() {
      indexLookup.clear();
   }

   /**
    * Gets the column index that corresponds to a specific colum name.
    * If the CSV file doesn't have a header row, this method will always return
    * null.
    *
    * @param name the column name
    * @return the column index, or null if the name doesn't exist
    */
   @Override
   public Integer getColumnIndex(String name) {
      if (null == header) {
         throw new IllegalStateException("The header row hasn't been read yet.");
      }

      createIndexLookup(header);

      return indexLookup.get(name);
   }

   /**
    * Gets the property descriptor for a given column position.
    * @param col the column to find the description for
    * @return - the property descriptor for the column position or null if one could not be found.
    * @throws IntrospectionException - thrown on error retrieving the property description.
    */
   @Override
   public PropertyDescriptor findDescriptor(int col) throws IntrospectionException {
      String columnName = getColumnName(col);
      return (StringUtil.trimToNull(columnName) != null) ? findDescriptor(columnName) : null;
   }

   /**
    * Gets the field for a given column position.
    *
    * @param col the column to find the field for
    * @return - BeanField containing the field - and whether it is mandatory - for a given column position, or null if
    * one could not be found
    */
   @Override
   public BeanField findField(int col) {
      String columnName = getColumnName(col);
      return (StringUtil.trimToNull(columnName) != null) ? findField(columnName) : null;
   }

   /**
    * Get the column name for a given column position.
    *
    * @param col - column position.
    * @return - the column name or null if the position is larger than the header array or there is no headers defined.
    */
   public String getColumnName(int col) {
      return ((null != header) && (col < header.length)) ? header[col] : null;
   }

   /**
    * Find the property descriptor for a given column.
    * @param name - column name to look up.
    * @return - the property descriptor for the column.
    * @throws IntrospectionException - thrown on error loading the property descriptors.
    */
   protected PropertyDescriptor findDescriptor(String name) throws IntrospectionException {
      if (null == descriptorMap) {
         descriptorMap = loadDescriptorMap(); //lazy load descriptors
      }
      return descriptorMap.get(name.toUpperCase().trim());
   }

   /**
    * Find the field for a given column.
    *
    * @param name - the column name to look up.
    * @return - BeanField containing the field - and whether it is mandatory - for the column.
    */
   protected BeanField findField(String name) {
      if (null == fieldMap) {
         fieldMap = loadFieldMap(); //lazy load fields
      }
      return fieldMap.get(name.toUpperCase().trim());
   }

   /**
    * Determines if the name of a property descriptor matches the column name.
    * Currently only used by unit tests.
    * @param name - name of the column.
    * @param desc - property descriptor to check against
    * @return - true if the name matches the name in the property descriptor.
    */
   protected boolean matches(String name, PropertyDescriptor desc) {
      return desc.getName().equals(name.trim());
   }

   /**
    * builds a map of property descriptors for the Bean.
    * @return - map of property descriptors
    * @throws IntrospectionException - thrown on error getting information about the bean.
    */
   protected Map<String, PropertyDescriptor> loadDescriptorMap() throws IntrospectionException {
      Map<String, PropertyDescriptor> map = new HashMap<>();

      PropertyDescriptor[] descriptors;
      descriptors = loadDescriptors(getType());
      for (PropertyDescriptor descriptor : descriptors) {
         map.put(descriptor.getName().toUpperCase().trim(), descriptor);
      }

      return map;
   }

   /**
    * Builds a map of fields (and whether they're mandatory) for the Bean.
    *
    * @return - a map of fields (and whether they're mandatory)
    */
   protected Map<String, BeanField> loadFieldMap() {
      Map<String, BeanField> map = new HashMap<>();

      for (Field field : loadFields(getType())) {
         boolean required = field.getAnnotation(CsvBind.class).required();
         map.put(field.getName().toUpperCase().trim(), new BeanField(field, required));
      }

      return map;
   }

   private PropertyDescriptor[] loadDescriptors(Class<T> cls) throws IntrospectionException {
      BeanInfo beanInfo = Introspector.getBeanInfo(cls);
      return beanInfo.getPropertyDescriptors();
   }

   private List<Field> loadFields(Class<T> cls) {
      List<Field> fields = new ArrayList<>();
      for (Field field : cls.getDeclaredFields()) {
         if (field.isAnnotationPresent(CsvBind.class)) {
            fields.add(field);
         }
      }
      return fields;
   }

   /**
    * Creates an object to be mapped.
    * @return an object of type T.
    * @throws ReflectiveOperationException - thrown on error creating object.
    */
   @Override
   public T createBean() throws ReflectiveOperationException {
      return type.getConstructor().newInstance();
   }

   /**
    * get the class type that the Strategy is mapping.
    * @return Class of the object that mapper will create.
    */
   public Class<T> getType() {
      return type;
   }

   /**
    * Sets the class type that is being mapped.
    *
    * @param type Class type.
    */
   public void setType(Class<T> type) {
      this.type = type;
   }

   /**
    * Determines whether the mapping strategy is driven by {@link liquibase.util.csv.opencsv.bean.CsvBind} annotations.
    *
    * @return whether the mapping strategy is driven by annotations
    */
   @Override
   public boolean isAnnotationDriven() {
      if (!determinedIfAnnotationDriven) { // lazy load this, and only calculate it once
         for (Field field : type.getDeclaredFields()) {
            if (field.isAnnotationPresent(CsvBind.class)) {
               annotationDriven = true;
               break;
            }
         }
         determinedIfAnnotationDriven = true;
      }
      return annotationDriven;
   }
}


package liquibase.integration.cdi;

import liquibase.Scope;
import liquibase.integration.cdi.annotations.LiquibaseSchema;
import liquibase.integration.cdi.exceptions.CyclicDependencyException;
import liquibase.integration.cdi.exceptions.DependencyNotFoundException;
import liquibase.logging.LogType;
import liquibase.logging.Logger;

import javax.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * @author Nikita Lipatov (https://github.com/islonik), antoermo (https://github.com/dikeert)
 * @since 27/5/17.
 */
@ApplicationScoped
public class SchemesTreeBuilder {
    private static final Logger log = Scope.getCurrentScope().getLog(SchemesTreeBuilder.class);

    private class SchemaNode {
        private final LiquibaseSchema item;

        private final Collection<SchemaNode> children = new ArrayList<>();

        public SchemaNode(LiquibaseSchema item) {
            this.item = item;
        }

        public LiquibaseSchema getItem() {
            return item;
        }

        public Collection<SchemaNode> getChildren() {
            return Collections.unmodifiableCollection(children);
        }

        public void addChild(LiquibaseSchema child) {
            children.add(new SchemaNode(child));
        }

        public SchemaNode find(String name) {
            SchemaNode result = null;
            if (this.item.name().equals(name)) {
                result = this;
            } else {
                for (SchemaNode child : children) {
                    SchemaNode found = child.find(name);
                    if ((result == null) && (found != null)) {
                        result = child.find(name);
                    } else if ((result != null) && (found != null)) {
                        throw new IllegalStateException(String.format(
                                "Duplicate schema names [%s] detected!",
                                result.getItem().name()));
                    }
                }
            }

            return result;
        }

        public List<LiquibaseSchema> toList() {
            List<LiquibaseSchema> list = new ArrayList<>(children.size() + 1);
            list.add(item);
            for (SchemaNode child : children) {
                list.addAll(child.toList());
            }
            return list;
        }
    }

    /**
     * Builds a collection of schemes sorted according dependencies
     *
     * @param schemes All found Liquibase Schema annotations in 'war' or 'ear' type file.
     * @return sorted collection of schemes
     */
    public List<LiquibaseSchema> build(final String id, Collection<LiquibaseSchema> schemes) {
        log.fine(LogType.LOG, String.format("[id = %s] build(%s)", id, schemes));

        log.info(LogType.LOG, String.format("[id = %s] Sorting schemes according dependencies...", id));

        if (schemes.isEmpty()) {
            return Collections.emptyList();
        }

        SchemaNode root = null;

        // first, copy schemes to no modify source collection
        schemes = new ArrayList<>(schemes);
        Collection<LiquibaseSchema> availableSchemes = new ArrayList<>(schemes);
        // then find not dependent schemes - this will the roots of hierarchy.

        List<LiquibaseSchema> notDependent = new ArrayList<>();
        for (LiquibaseSchema liquibaseSchema : schemes) {
            String depends = liquibaseSchema.depends();
            if (depends.trim().isEmpty()) {
                notDependent.add(liquibaseSchema);
            }
        }

        log.info(LogType.LOG, String.format("[id = %s] Found [%s] not dependent schemes.", id, notDependent.size()));

        if (notDependent.isEmpty()) { // if there is no not-dependent schema, then there is a cyclic dependency.
            throw new CyclicDependencyException(String.format("[id = %s] Not independent schemes, possible cyclic dependencies discovered.", id));
        } else {
            // take first of not-dependent and use it as root of hierarchy.
            root = new SchemaNode(notDependent.get(0));
            log.fine(LogType.LOG, String.format("[id = %s] Selected dependencies tree root [%s]", id, root.getItem()));
            availableSchemes.removeAll(notDependent); // we won't to check not-dependent schemes.
            notDependent.remove(root.getItem());  // remove root from not-dependent schemes
            schemes.retainAll(availableSchemes); // remove not-dependent from all schemes

            // now make all not-dependent schemes children to selected root.
            for (LiquibaseSchema liquibaseSchema : notDependent) {
                root.addChild(liquibaseSchema);
            }

            log.fine(LogType.LOG, String.format("[id = %s] Made other non-dependent schemes children of root. [%s] dependent schemes to resolve. Resolving...",
                    id,
                    availableSchemes.size()
            ));

            int cycles = 0;
            long start = System.currentTimeMillis();
            // until we resolve all dependencies
            while (!availableSchemes.isEmpty()) {
                cycles++;
                log.fine(LogType.LOG, String.format("[id = %s] Resolution cycle [%s] started.", id, cycles));
                int additions = 0; //we will count dependencies resolution for each resolution cycle.
                for (LiquibaseSchema liquibaseSchema : schemes) {
                    log.fine(LogType.LOG, String.format(
                            "[id = %s] LiquibaseSchema [name=%s] depends on liquibaseSchema [name=%s].",
                            id, liquibaseSchema.name(), liquibaseSchema.depends()
                    ));
                    SchemaNode parent = root.find(liquibaseSchema.depends());

                    // we make the dependent liquibaseSchema as a child for it's dependency if found. If not, we just continue.
                    if (parent == null) {
                        log.fine(LogType.LOG, String.format(
                                "[id = %s] Dependency not found in resolved dependencies tree, skipping liquibaseSchema [name=%s] for a while.",
                                id, liquibaseSchema.name()
                        ));

                        boolean isDependencyMissed = true;
                        for (LiquibaseSchema tmpLiquibaseSchema : availableSchemes) {
                            if (tmpLiquibaseSchema.name().equalsIgnoreCase(liquibaseSchema.depends())) {
                                isDependencyMissed = false;
                                break;
                            }
                        }
                        if (isDependencyMissed) {
                            throw new DependencyNotFoundException(String.format(
                                    "[id = %s][name=%s] depends on [name=%s], but it is not found!",
                                    id, liquibaseSchema.name(), liquibaseSchema.depends()
                            ));
                        }
                    } else {
                        log.fine(LogType.LOG, String.format(
                                "[id = %s] Dependency found for liquibaseSchema [name=%s], moving it to resolved dependencies tree.",
                                id, liquibaseSchema.name()
                        ));
                        parent.addChild(liquibaseSchema);
                        availableSchemes.remove(liquibaseSchema);
                        additions++;
                    }
                }
                log.fine(LogType.LOG, String.format("[id = %s] Resolution cycle [%s] completed", id, cycles));

                //if not resolutions happened through resolution cycle, definitely there is a cyclic dependency.
                if (additions == 0) {
                    throw new CyclicDependencyException(String.format("[id = %s] Cyclic dependencies discovered!", id));
                }

                schemes.retainAll(availableSchemes);
            }

            log.info(LogType.LOG, String.format("[id = %s] Dependencies resolved in [cycles=%s, millis=%s]", id, cycles, System.currentTimeMillis() - start));
        }

        return root.toList();
    }
}


package liquibase.util.csv.opencsv.bean;

import liquibase.util.csv.opencsv.CSVReader;

import java.io.IOException;

/**
 * Allows for the mapping of columns with their positions.  Using this strategy requires all the columns
 * to be present in the csv file and for them to be in a particular order.   Also this strategy requires
 * that the file does NOT have an header.  That said the main use of this strategy is files that
 * do not have headers.
 *
 * @param <T> - Type of object that is being processed.
 */

public class ColumnPositionMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {
   private String[] columnMapping = new String[]{};

   /**
    * Default Constructor.
    */
   public ColumnPositionMappingStrategy() {
   }

   /**
    * Captures the header from the reader - required by the MappingStrategy interface and is a do
    * nothing method for the ColumnPositionMappingStrategy.
    *
    * @param reader - CSVReader.
    * @throws IOException - would be thrown by the CSVReader if it was used :)
    */
   public void captureHeader(CSVReader reader) throws IOException {
      //do nothing, first line is not header
   }

   /**
    * gets the column position for a given column name.
    * @param name the column name
    * @return - column position or null if the name does not map.
    */
   @Override
   public Integer getColumnIndex(String name) {
      return indexLookup.get(name);
   }

   /**
    * gets a column name.
    * @param col - position of the column.
    * @return - column name or null if col > number of mappings.
    */
   @Override
   public String getColumnName(int col) {
      return (col < columnMapping.length) ? columnMapping[col] : null;
   }

   /**
    * Retrieves the column mappings.
    * @return - String array with the column mappings.
    */
   public String[] getColumnMapping() {
      return columnMapping.clone();
   }

   /**
    * Setter for the ColumnMappings.
    * @param columnMapping - column names to be mapped.
    */
   public void setColumnMapping(String... columnMapping) {
       this.columnMapping = (columnMapping != null) ? columnMapping.clone() : new String[]{};
      resetIndexMap();
      createIndexLookup(this.columnMapping);
   }
}


package liquibase.util;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

/**
 * Various utility methods for working with XML.
 */
public class XMLUtil {
    /**
     * Extracts the text from the given element.
     * Element.getTextContet() is java5 specific, so we need to use this until we drop 1.4 support.
     */
    public static String getTextContent(Node element) {
        StringBuffer text = new StringBuffer();
        NodeList childNodes = element.getChildNodes();
        for (int i=0; i< childNodes.getLength(); i++) {
            Node child = childNodes.item(i);
            if (child instanceof Text) {
                text.append(child.getNodeValue());
            }
        }

        return text.toString();
    }
}


package liquibase.snapshot.jvm;

import liquibase.CatalogAndSchema;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.Database;
import liquibase.database.core.InformixDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.exception.DatabaseException;
import liquibase.snapshot.CachedRow;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.JdbcDatabaseSnapshot;
import liquibase.statement.core.GetViewDefinitionStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Schema;
import liquibase.structure.core.View;
import liquibase.util.StringUtil;

import java.sql.SQLException;
import java.util.List;

public class ViewSnapshotGenerator extends JdbcSnapshotGenerator {

    public ViewSnapshotGenerator() {
        super(View.class, new Class[] { Schema.class });
    }

//    public Boolean has(DatabaseObject example, DatabaseSnapshot snapshot, SnapshotGeneratorChain chain) throws DatabaseException {
//        Database database = snapshot.getDatabase();
//        if (!(example instanceof View)) {
//            return chain.has(example, snapshot);
//        }
//        String viewName = example.getName();
//        Schema schema = example.getSchema();
//        try {
//            ResultSet rs = getMetaDataFromCache(database).getTables(database.getJdbcCatalogName(schema), database.getJdbcSchemaName(schema), database.correctObjectName(viewName, View.class), new String[]{"VIEW"});
//            try {
//                return rs.next();
//            } finally {
//                try {
//                    rs.close();
//                } catch (SQLException ignore) {
//                }
//            }
//        } catch (SQLException e) {
//            throw new DatabaseException(e);
//        }
//    }

    @Override
    protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException {
        if (((View) example).getDefinition() != null) {
            return example;
        }
        Database database = snapshot.getDatabase();
        Schema schema = example.getSchema();

        List<CachedRow> viewsMetadataRs = null;
        try {
            viewsMetadataRs = ((JdbcDatabaseSnapshot) snapshot).getMetaDataFromCache().getViews(((AbstractJdbcDatabase) database).getJdbcCatalogName(schema), ((AbstractJdbcDatabase) database).getJdbcSchemaName(schema), example.getName());
            if (!viewsMetadataRs.isEmpty()) {
                CachedRow row = viewsMetadataRs.get(0);
                String rawViewName = row.getString("TABLE_NAME");
                String rawSchemaName = StringUtil.trimToNull(row.getString("TABLE_SCHEM"));
                String rawCatalogName = StringUtil.trimToNull(row.getString("TABLE_CAT"));
                String remarks = row.getString("REMARKS");
                if (remarks != null) {
                    remarks = remarks.replace("''", "'"); //come back escaped sometimes
                }

                View view = new View().setName(cleanNameFromDatabase(rawViewName, database));
                view.setRemarks(remarks);
                CatalogAndSchema schemaFromJdbcInfo = ((AbstractJdbcDatabase) database).getSchemaFromJdbcInfo(rawCatalogName, rawSchemaName);
                view.setSchema(new Schema(schemaFromJdbcInfo.getCatalogName(), schemaFromJdbcInfo.getSchemaName()));

                try {
                    String definition = database.getViewDefinition(schemaFromJdbcInfo, view.getName());

                    if (definition.startsWith("FULL_DEFINITION: ")) {
                        definition = definition.replaceFirst("^FULL_DEFINITION: ", "");
                        view.setContainsFullDefinition(true);
                    }

                    // remove strange zero-termination seen on some Oracle view definitions
                    int length = definition.length();
                    if (definition.charAt(length-1) == 0) {
                      definition = definition.substring(0, length-1);
                    }

                    if (database instanceof InformixDatabase) {
                        // Cleanup
                        definition = definition.trim();
                        definition = definition.replaceAll("\\s*,\\s*", ", ");
                        definition = definition.replaceAll("\\s*;", "");

                        // Strip the schema definition because it can optionally be included in the tag attribute
                        definition = definition.replaceAll("(?i)\""+view.getSchema().getName()+"\"\\.", "");
                    }

                    view.setDefinition(definition);
                } catch (DatabaseException e) {
                    throw new DatabaseException("Error getting " + database.getConnection().getURL() + " view with " + new GetViewDefinitionStatement(view.getSchema().getCatalogName(), view.getSchema().getName(), rawViewName), e);
                }

                return view;
            } else {
                return null;
            }
        } catch (SQLException e) {
            throw new DatabaseException(e);
        }
    }

    @Override
    protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
        if (!snapshot.getSnapshotControl().shouldInclude(View.class)) {
            return;
        }

        if (foundObject instanceof Schema) {
            Schema schema = (Schema) foundObject;
            Database database = snapshot.getDatabase();
            List<CachedRow> viewsMetadataRs = null;
            try {
                viewsMetadataRs = ((JdbcDatabaseSnapshot) snapshot).getMetaDataFromCache().getViews(((AbstractJdbcDatabase) database).getJdbcCatalogName(schema), ((AbstractJdbcDatabase) database).getJdbcSchemaName(schema), null);
                for (CachedRow row : viewsMetadataRs) {
                    CatalogAndSchema catalogAndSchema = ((AbstractJdbcDatabase) database).getSchemaFromJdbcInfo(row.getString("TABLE_CAT"), row.getString("TABLE_SCHEM"));
                    View view = new View();
                    view.setName(row.getString("TABLE_NAME"));
                    view.setSchema(new Schema(catalogAndSchema.getCatalogName(), catalogAndSchema.getSchemaName()));
                    view.setRemarks(row.getString("REMARKS"));
                    view.setDefinition(row.getString("OBJECT_BODY"));
                    if(database instanceof OracleDatabase) {
                        view.setAttribute("editioning", "Y".equals(row.getString("EDITIONING_VIEW")));
                    }
                    schema.addDatabaseObject(view);
                }
            } catch (SQLException e) {
                throw new DatabaseException(e);
            }
        }
    }

    //from SQLIteSnapshotGenerator
    //    protected void readViews(DatabaseSnapshot snapshot, String schema, DatabaseMetaData databaseMetaData) throws SQLException, DatabaseException {
//
//        Database database = snapshot.getDatabase();
//
//        updateListeners("Reading tables for " + database.toString() + " ...");
//        ResultSet rs = databaseMetaData.getTables(
//                database.convertRequestedSchemaToCatalog(schema),
//                database.convertRequestedSchemaToSchema(schema),
//                null,
//                new String[]{"TABLE", "VIEW"});
//
//        try {
//            while (rs.next()) {
//                String type = rs.getString("TABLE_TYPE");
//                String name = rs.getString("TABLE_NAME");
//                String schemaName = rs.getString("TABLE_SCHEM");
//                String catalogName = rs.getString("TABLE_CAT");
//                String remarks = rs.getString("REMARKS");
//
//                if (database.isSystemTable(catalogName, schemaName, name) ||
//                        database.isLiquibaseTable(name) ||
//                        database.isSystemView(catalogName, schemaName, name)) {
//                    continue;
//                }
//
//                if ("TABLE".equals(type)) {
//                    Table table = new Table(name);
//                    table.setRemarks(StringUtil.trimToNull(remarks));
//                    table.setDatabase(database);
//                    table.setSchema(schemaName);
//                    snapshot.getTables().add(table);
//                } else if ("VIEW".equals(type)) {
//                    View view = new View(name);
//                    view.setSchema(schemaName);
//                    try {
//                        view.setDefinition(database.
//                                getViewDefinition(schema, name));
//                    } catch (DatabaseException e) {
//                        System.out.println("Error getting view with " + new GetViewDefinitionStatement(schema, name));
//                        throw e;
//                    }
//                    snapshot.getViews().add(view);
//                }
//            }
//        } finally {
//            rs.close();
//        }
//    }
}


package liquibase.datatype.core;

import liquibase.change.core.LoadDataChange;
import liquibase.database.Database;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.PostgresDatabase;
import liquibase.datatype.DataTypeInfo;
import liquibase.datatype.DatabaseDataType;
import liquibase.datatype.LiquibaseDataType;
import liquibase.statement.DatabaseFunction;
import liquibase.util.StringUtil;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Locale;

@DataTypeInfo(name="char", aliases = {"java.sql.Types.CHAR", "bpchar"}, minParameters = 0, maxParameters = 1, priority = LiquibaseDataType.PRIORITY_DEFAULT)
public class CharType extends LiquibaseDataType {
    @Override
    public DatabaseDataType toDatabaseDataType(Database database) {
        if (database instanceof MSSQLDatabase) {
            Object[] parameters = getParameters();
            if (parameters.length > 0) {
                String param1 = parameters[0].toString();
                if (!param1.matches("\\d+") || (new BigInteger(param1).compareTo(BigInteger.valueOf(8000)) > 0)) {

                    DatabaseDataType type = new DatabaseDataType(database.escapeDataTypeName("char"), 8000);
                    type.addAdditionalInformation(getAdditionalInformation());
                    return type;
                }
            }
            if (parameters.length == 0) {
                parameters = new Object[] { 1 };
            } else if (parameters.length > 1) {
                parameters = Arrays.copyOfRange(parameters, 0, 1);
            }
            DatabaseDataType type = new DatabaseDataType(database.escapeDataTypeName("char"), parameters);
            type.addAdditionalInformation(getAdditionalInformation());
            return type;
        } else if (database instanceof PostgresDatabase){
            if ((getParameters() != null) && (getParameters().length == 1) && "2147483647".equals(getParameters()[0]
                .toString())) {
                DatabaseDataType type = new DatabaseDataType("CHARACTER");
                type.addAdditionalInformation("VARYING");
                return type;
            }
            return super.toDatabaseDataType(database);
        }

        return super.toDatabaseDataType(database);
    }

    @Override
    public String objectToSql(Object value, Database database) {
        if ((value == null) || "null".equals(value.toString().toLowerCase(Locale.US))) {
            return null;
        }

        if (value instanceof DatabaseFunction) {
            return value.toString();
        }

        String val = String.valueOf(value);
        if ((database instanceof MSSQLDatabase) && !StringUtil.isAscii(val)) {
            return "N'"+database.escapeStringForDatabase(val)+"'";
        }

        return "'"+database.escapeStringForDatabase(val)+"'";
    }

    /**
     * Return the size of this data type definition. If unknown or unspecified, return -1
     */
    protected int getSize() {
        if (getParameters().length == 0) {
            return -1;
        }

        if (getParameters()[0] instanceof String) {
            return Integer.parseInt((String) getParameters()[0]);
        }

        if (getParameters()[0] instanceof Number) {
            return ((Number) getParameters()[0]).intValue();
        }

        return -1;
    }

    @Override
    public LoadDataChange.LOAD_DATA_TYPE getLoadTypeName() {
        return LoadDataChange.LOAD_DATA_TYPE.STRING;
    }

}


package liquibase.database.core;

import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.DatabaseConnection;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.exception.DatabaseException;
import liquibase.exception.DateParseException;
import liquibase.structure.DatabaseObject;
import liquibase.util.ISODateFormat;

import java.math.BigInteger;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

import static liquibase.statement.DatabaseFunction.CURRENT_DATE_TIME_PLACE_HOLDER;

public class HsqlDatabase extends AbstractJdbcDatabase {
    private static final Map<String, HashSet<String>> SUPPORTED_DEFAULT_VALUE_COMPUTED_MAP;
    private static String START_CONCAT = "CONCAT(";
    private static String END_CONCAT = ")";
    private static String SEP_CONCAT = ", ";
    private static List keywords = Arrays.asList(
            "ADD",
            "ALL",
            "ALLOCATE",
            "ALTER",
            "AND",
            "ANY",
            "ARE",
            "ARRAY",
            "AS",
            "ASENSITIVE",
            "ASYMMETRIC",
            "AT",
            "ATOMIC",
            "AUTHORIZATION",
            "AVG",
            "BEGIN",
            "BETWEEN",
            "BIGINT",
            "BINARY",
            "BLOB",
            "BOOLEAN",
            "BOTH",
            "BY",
            "CALL",
            "CALLED",
            "CASCADED",
            "CASE",
            "CAST",
            "CHAR",
            "CHARACTER",
            "CHECK",
            "CLOB",
            "CLOSE",
            "COLLATE",
            "COLUMN",
            "COMMIT",
            "CONDITION",
            "CONNECT",
            "CONSTRAINT",
            "CONTINUE",
            "CORRESPONDING",
            "COUNT",
            "CREATE",
            "CROSS",
            "CUBE",
            "CURRENT",
            "CURRENT_DATE",
            "CURRENT_DEFAULT_TRANSFORM_GRO",
            "CURRENT_PATH",
            "CURRENT_ROLE",
            "CURRENT_TIME",
            "CURRENT_TIMESTAMP",
            "CURRENT_TRANSFORM_GROUP_FOR_T",
            "CURRENT_USER",
            "CURSOR",
            "DATE",
            "DAY",
            "DEALLOCATE",
            "DEC",
            "DECIMAL",
            "DECLARE",
            "DEFAULT",
            "DELETE",
            "DEREF",
            "DESCRIBE",
            "DETERMINISTIC",
            "DISCONNECT",
            "DISTINCT",
            "DO",
            "DOUBLE",
            "DROP",
            "DYNAMIC",
            "EACH",
            "ELEMENT",
            "ELSE",
            "ELSEIF",
            "END",
            "ESCAPE",
            "EXCEPT",
            "EXEC",
            "EXECUTE",
            "EXISTS",
            "EXIT",
            "EXTERNAL",
            "FALSE",
            "FETCH",
            "FILTER",
            "FLOAT",
            "FOR",
            "FOREIGN",
            "FREE",
            "FROM",
            "FULL",
            "FUNCTION",
            "GET",
            "GLOBAL",
            "GRANT",
            "GROUP",
            "HAVING",
            "HOLD",
            "HOUR",
            "IDENTITY",
            "IF",
            "IMMEDIATE",
            "IN",
            "INDICATOR",
            "INNER",
            "INOUT",
            "INPUT",
            "INSENSITIVE",
            "INSERT",
            "INT",
            "INTEGER",
            "INTERSECT",
            "INTERVAL",
            "INTO",
            "IS",
            "ITERATE",
            "JOIN",
            "LANGUAGE",
            "LARGE",
            "LEADING",
            "LEAVE",
            "LEFT",
            "LIKE",
            "LOCAL",
            "LOCALTIME",
            "LOCALTIMESTAMP",
            "LOOP",
            "MATCH",
            "MAX",
            "MEMBER",
            "MERGE",
            "METHOD",
            "MIN",
            "MINUTE",
            "MODIFIES",
            "MODULE",
            "MONTH",
            "MULTISET",
            "NATIONAL",
            "NATURAL",
            "NCHAR",
            "NCLOB",
            "NEW",
            "NO",
            "NONE",
            "NOT",
            "NULL",
            "NUMERIC",
            "OF",
            "ON",
            "ONLY",
            "OPEN",
            "OR",
            "ORDER",
            "OUT",
            "OUTER",
            "OUTPUT",
            "OVER",
            "OVERLAPS",
            "PARAMETER",
            "PARTITION",
            "PRECISION",
            "PREPARE",
            "PRIMARY",
            "PROCEDURE",
            "RANGE",
            "READS",
            "REAL",
            "RECURSIVE",
            "REF",
            "REFERENCES",
            "REFERENCING",
            "RELEASE",
            "REPEAT",
            "RESIGNAL",
            "RESULT",
            "RETURN",
            "RETURNS",
            "REVOKE",
            "RIGHT",
            "ROLLBACK",
            "ROLLUP",
            "ROW",
            "ROWS",
            "SAVEPOINT",
            "SCOPE",
            "SCROLL",
            "SEARCH",
            "SELECT",
            "SENSITIVE",
            "SESSION_USER",
            "SET",
            "SIGNAL",
            "SIMILAR",
            "SMALLINT",
            "SOME",
            "SPECIFIC",
            "SPECIFICTYPE",
            "SQL",
            "SQLEXCEPTION",
            "SQLSTATE",
            "SQLWARNING",
            "START",
            "STATIC",
            "SUBMULTISET",
            "SUM",
            "SYMMETRIC",
            "SYSTEM",
            "SYSTEM_USER",
            "TABLE",
            "TABLESAMPLE",
            "THEN",
            "TIME",
            "TIMESTAMP",
            "TIMEZONE_HOUR",
            "TIMEZONE_MINUTE",
            "TO",
            "TRAILING",
            "TREAT",
            "TRIGGER",
            "TRUE",
            "UNDO",
            "UNION",
            "UNIQUE",
            "UNNEST",
            "UNTIL",
            "UPDATE",
            "USER",
            "USING",
            "VALUE",
            "VALUES",
            "VARCHAR",
            "VARYING",
            "WHEN",
            "WHENEVER",
            "WHERE",
            "WHILE",
            "WINDOW",
            "WITHIN",
            "WITHOUT",
            "YEAR",
            "ALIAS",
            "AUTOCOMMIT",
            "CACHED",
            "CHECKPOINT",
            "EXPLAIN",
            "IGNORECASE",
            "INDEX",
            "LOGSIZE",
            "MATCHED",
            "MAXROWS",
            "MEMORY",
            "MINUS",
            "NEXT",
            "OPENBRACKET",
            "PLAN",
            "PROPERTY",
            "READONLY",
            "REFERENTIAL_INTEGRITY",
            "RENAME",
            "RESTART",
            "SCRIPT",
            "SCRIPTFORMAT",
            "SEMICOLON",
            "SHUTDOWN",
            "TEMP",
            "TEXT",
            "VIEW",
            "WRITE_DELAY",
            "VAR_POP",
            "VAR_SAMP",
            "STDDEV_POP",
            "STDDEV_SAMP",
            "DEFRAG",
            "INCREMENT",
            "TOCHAR",
            "DATABASE",
            "SCHEMA",
            "ROLE",
            "DOW",
            "INITIAL");

    static {
        Map<String, HashSet<String>> tempMap = new HashMap<>();
        tempMap.put("datetime", new HashSet<>(Arrays.asList("CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "TODAY", "NOW", CURRENT_DATE_TIME_PLACE_HOLDER)));
        SUPPORTED_DEFAULT_VALUE_COMPUTED_MAP = Collections.unmodifiableMap(tempMap);
    }

    private Boolean oracleSyntax;

    public HsqlDatabase() {
        super.unquotedObjectsAreUppercased = true;
        super.setCurrentDateTimeFunction("NOW");
        super.sequenceNextValueFunction = "NEXT VALUE FOR %s";
        super.defaultAutoIncrementStartWith = BigInteger.ZERO;
        super.sequenceCurrentValueFunction = "CURRVAL('%s')";
    }

    /**
     * Checks to see if the string is an acceptable computed value for HSQL
     * "datetime" columns are the only columns for which HSQL supports computer values.
     *
     * @param columnType   String of the column's data type
     * @param defaultValue String to be checked for valid valueComputed in HSQL
     * @return boolean True if the string represents a function supported by HSQL for default values
     */
    public static boolean supportsDefaultValueComputed(String columnType, String defaultValue) {
        HashSet<String> possibleComputedValues = SUPPORTED_DEFAULT_VALUE_COMPUTED_MAP.get(columnType);
        return (possibleComputedValues != null) && possibleComputedValues.contains(defaultValue.toLowerCase());
    }

    @Override
    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
        return "HSQL Database Engine".equalsIgnoreCase(conn.getDatabaseProductName());
    }

    @Override
    public String getDefaultDriver(String url) {
        if (url.startsWith("jdbc:hsqldb:")) {
            return "org.hsqldb.jdbcDriver";
        }
        return null;
    }

    @Override
    public Integer getDefaultPort() {
        return 9001;
    }

    @Override
    protected String getDefaultDatabaseProductName() {
        return "HyperSQL";
    }

    @Override
    public int getPriority() {
        return PRIORITY_DEFAULT;
    }

    @Override
    public String getShortName() {
        return "hsqldb";
    }

    @Override
    public boolean supportsSequences() {
        return true;
    }

    @Override
    public boolean supportsInitiallyDeferrableColumns() {
        return false;
    }

    @Override
    public boolean supportsCatalogs() {
        try {
            return getDatabaseMajorVersion() >= 2;
        } catch (DatabaseException e) {
            return true;
        }
    }

    @Override
    protected String getConnectionCatalogName() throws DatabaseException {
        if (supportsCatalogs()) {
            return "PUBLIC";
        } else {
            return null;
        }

    }

    @Override
    protected String getConnectionSchemaName() {
        return "PUBLIC";
    }

    @Override
    public String getConcatSql(String... values) {
        if (values == null) {
            return null;
        }

        return getConcatSql(Arrays.asList(values));
    }

    /**
     * Recursive way of building CONCAT instruction
     *
     * @param values a non null List of String
     * @return a String containing the CONCAT instruction with all elements, or only a value if there is only one element in the list
     */
    private String getConcatSql(List<String> values) {
        if (values.size() == 1) {
            return values.get(0);
        } else {
            return START_CONCAT + values.get(0) + SEP_CONCAT + getConcatSql(values.subList(1, values.size())) + END_CONCAT;
        }
    }

    @Override
    public String getDateLiteral(String isoDate) {
        String returnString = isoDate;
        try {
            if (isDateTime(isoDate)) {
                ISODateFormat isoTimestampFormat = new ISODateFormat();
                DateFormat dbTimestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                returnString = dbTimestampFormat.format(isoTimestampFormat.parse(isoDate));
            }
        } catch (ParseException e) {
            throw new RuntimeException("Unexpected date format: " + isoDate, e);
        }
        return "'" + returnString + "'";
    }

    @Override
    public Date parseDate(String dateAsString) throws DateParseException {
        try {
            if (dateAsString.indexOf(" ") > 0) {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").parse(dateAsString);
            } else {
                if (dateAsString.indexOf(":") > 0) {
                    return new SimpleDateFormat("HH:mm:ss").parse(dateAsString);
                } else {
                    return new SimpleDateFormat("yyyy-MM-dd").parse(dateAsString);
                }
            }
        } catch (ParseException e) {
            throw new DateParseException(dateAsString);
        }
    }

    @Override
    public boolean supportsTablespaces() {
        return false;
    }

    @Override
    public boolean isReservedWord(String value) {
        return keywords.contains(value.toUpperCase());
    }

    @Override
    public boolean isCaseSensitive() {
        return false;
    }

    @Override
    public void setConnection(DatabaseConnection conn) {
        oracleSyntax = null;
        super.setConnection(conn);
    }

    public boolean isUsingOracleSyntax() {
        if (oracleSyntax == null) {
            oracleSyntax = Boolean.FALSE;
            if ((getConnection() != null) && (getConnection().getURL() != null)) {
                for (String str : getConnection().getURL().split(";")) {
                    if (str.contains("sql.syntax_ora") && str.contains("=")) {
                        oracleSyntax = Boolean.valueOf(str.split("=")[1].trim());
                        break;
                    }
                }
            }
        }
        return oracleSyntax;
    }

    @Override
    public String escapeObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
        if (quotingStrategy == ObjectQuotingStrategy.QUOTE_ALL_OBJECTS) {
            return super.escapeObjectName(objectName, objectType);
        }
        if ((objectName != null) && (quotingStrategy != ObjectQuotingStrategy.QUOTE_ALL_OBJECTS) && isReservedWord
            (objectName.toUpperCase(Locale.US))) {
                return "\""+objectName.toUpperCase(Locale.US)+"\"";
        }
        return objectName;
    }

    @Override
    public int getMaxFractionalDigitsForTimestamp() {
        // HyperSQL does not seem to specify the exact number of possible fractional digits in its documentation,
        // this value is derived from tests.
        return 9;
    }
}


package liquibase.changelog.visitor;

import liquibase.changelog.*;
import liquibase.changelog.filter.ChangeSetFilterResult;
import liquibase.changelog.filter.NotInChangeLogChangeSetFilter;
import liquibase.database.Database;
import liquibase.exception.LiquibaseException;

import java.util.*;

/**
 * ChangeSetVisitor that will collect the execution status of changeSets without executing them. Also includes changeSets
 * previously executed against the database but no longer in the change log.
 */
public class StatusVisitor implements ChangeSetVisitor, SkippedChangeSetVisitor {

    private LinkedHashMap<ChangeSet, ChangeSetStatus> changeSetStatuses = new LinkedHashMap<>();
    private final List<RanChangeSet> ranChangeSets;

    public StatusVisitor(Database database) throws LiquibaseException {
        ranChangeSets = new ArrayList<>(ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).getRanChangeSets());
    }

    @Override
    public Direction getDirection() {
        return Direction.FORWARD;
    }

    @Override
    public void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database, Set<ChangeSetFilterResult> filterResults) throws LiquibaseException {
        ChangeSetStatus status = addStatus(changeSet, databaseChangeLog, database);
        status.setWillRun(true);
        status.setFilterResults(filterResults);
    }

    @Override
    public void skipped(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database, Set<ChangeSetFilterResult> filterResults) throws LiquibaseException {
        ChangeSetStatus status = addStatus(changeSet, databaseChangeLog, database);
        status.setWillRun(false);
        status.setFilterResults(filterResults);
    }

    protected ChangeSetStatus addStatus(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database) throws LiquibaseException {
        ChangeSetStatus status = new ChangeSetStatus(changeSet);

        RanChangeSet ranChangeSetToRemove = null;
        for (RanChangeSet ranChangeSet : ranChangeSets) {
            if (ranChangeSet.isSameAs(changeSet)) {
                status.setPreviouslyRan(true);
                status.setDateLastExecuted(ranChangeSet.getDateExecuted());
                status.setStoredCheckSum(ranChangeSet.getLastCheckSum());
                status.setRanChangeSet(ranChangeSet);

                ranChangeSetToRemove = ranChangeSet;

                break;
            }
        }
        if (ranChangeSetToRemove != null) {
            ranChangeSets.remove(ranChangeSetToRemove);
        }

        changeSetStatuses.put(changeSet, status);

        return status;
    }

    /**
     * Convenience method to return the ChangeSetStatus of a given changeSet. Returns null if the changeSet is not know.
     */
    public ChangeSetStatus getStatus(ChangeSet changeSet) {
        return changeSetStatuses.get(changeSet);
    }

    /**
     * Return the status of all changeSets, in the order they exist in the databasechangelog.
     * Any change sets not in the current change log but previously ran against the database will be at the front of the List
     * with a not run reason type of {@link liquibase.changelog.filter.NotInChangeLogChangeSetFilter}
     */
    public List<ChangeSetStatus> getStatuses() {
        ArrayList<ChangeSetStatus> returnList = new ArrayList<>();
        for (RanChangeSet changeSet : ranChangeSets) {
            ChangeSetStatus status = new ChangeSetStatus(new ChangeSet(changeSet.getId(), changeSet.getAuthor(), false, false, changeSet.getChangeLog(), null, null, null));
            status.setPreviouslyRan(true);
            status.setDateLastExecuted(changeSet.getDateExecuted());
            status.setStoredCheckSum(changeSet.getLastCheckSum());

            status.setComments(changeSet.getComments());
            status.setDescription(changeSet.getDescription());
            status.setWillRun(false);
            status.setFilterResults(new HashSet<>(Arrays.asList(new ChangeSetFilterResult(false, "Change set is not in change log", NotInChangeLogChangeSetFilter.class))));
            status.setRanChangeSet(changeSet);

            returnList.add(status);
        }

        returnList.addAll(changeSetStatuses.values());

        return returnList;
    }

    /**
     * Return the change sets that will execute
     */
    public List<ChangeSetStatus> getChangeSetsToRun() {
        ArrayList<ChangeSetStatus> returnList = new ArrayList<>();
        for (ChangeSetStatus status : changeSetStatuses.values()) {
            if (status.getWillRun()) {
                returnList.add(status);
            }
        }

        return returnList;
    }

    /**
     * Return the change sets that will NOT execute
     */
    public List<ChangeSetStatus> getChangeSetsToSkip() {
        ArrayList<ChangeSetStatus> returnList = new ArrayList<>();
        for (ChangeSetStatus status : changeSetStatuses.values()) {
            if (!status.getWillRun()) {
                returnList.add(status);
            }
        }

        return returnList;
    }
}