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

import javax.jcr.Property;
import javax.jcr.Session;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFormatException;
import javax.jcr.PropertyType;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyIterator;
import javax.jcr.Value;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.IOException;

/**
 * This class provides various utility methods that are used by the property
 * test cases.
 */
public class PropertyUtil {


    public static final	String BASE_CHAR =
        "\\u0041-\\u005A|\\u0061-\\u007A|\\u00C0-\\u00D6|\\u00D8-\\u00F6|\\u00F8-\\u00FF|\\u0100-\\u0131|\\u0134-\\u013E|" +
        "\\u0141-\\u0148|\\u014A-\\u017E|\\u0180-\\u01C3|\\u01CD-\\u01F0|\\u01F4-\\u01F5|\\u01FA-\\u0217|\\u0250-\\u02A8|" +
        "\\u02BB-\\u02C1|\\u0386|\\u0388-\\u038A|\\u038C|\\u038E-\\u03A1|\\u03A3-\\u03CE|\\u03D0-\\u03D6|\\u03DA|\\u03DC|" +
        "\\u03DE|\\u03E0|\\u03E2-\\u03F3|\\u0401-\\u040C|\\u040E-\\u044F|\\u0451-\\u045C|\\u045E-\\u0481|\\u0490-\\u04C4|" +
        "\\u04C7-\\u04C8|\\u04CB-\\u04CC|\\u04D0-\\u04EB|\\u04EE-\\u04F5|\\u04F8-\\u04F9|\\u0531-\\u0556|\\u0559|" +
        "\\u0561-\\u0586|\\u05D0-\\u05EA|\\u05F0-\\u05F2|\\u0621-\\u063A|\\u0641-\\u064A|\\u0671-\\u06B7|\\u06BA-\\u06BE|" +
        "\\u06C0-\\u06CE|\\u06D0-\\u06D3|\\u06D5|\\u06E5-\\u06E6|\\u0905-\\u0939|\\u093D|\\u0958-\\u0961|\\u0985-\\u098C|" +
        "\\u098F-\\u0990|\\u0993-\\u09A8|\\u09AA-\\u09B0|\\u09B2|\\u09B6-\\u09B9|\\u09DC-\\u09DD|\\u09DF-\\u09E1|" +
        "\\u09F0-\\u09F1|\\u0A05-\\u0A0A|\\u0A0F-\\u0A10|\\u0A13-\\u0A28|\\u0A2A-\\u0A30|\\u0A32-\\u0A33|\\u0A35-\\u0A36|" +
        "\\u0A38-\\u0A39|\\u0A59-\\u0A5C|\\u0A5E|\\u0A72-\\u0A74|\\u0A85-\\u0A8B|\\u0A8D|\\u0A8F-\\u0A91|\\u0A93-\\u0AA8|" +
        "\\u0AAA-\\u0AB0|\\u0AB2-\\u0AB3|\\u0AB5-\\u0AB9|\\u0ABD|\\u0AE0|\\u0B05-\\u0B0C|\\u0B0F-\\u0B10|\\u0B13-\\u0B28|" +
        "\\u0B2A-\\u0B30|\\u0B32-\\u0B33|\\u0B36-\\u0B39|\\u0B3D|\\u0B5C-\\u0B5D|\\u0B5F-\\u0B61|\\u0B85-\\u0B8A|" +
        "\\u0B8E-\\u0B90|\\u0B92-\\u0B95|\\u0B99-\\u0B9A|\\u0B9C|\\u0B9E-\\u0B9F|\\u0BA3-\\u0BA4|\\u0BA8-\\u0BAA|" +
        "\\u0BAE-\\u0BB5|\\u0BB7-\\u0BB9|\\u0C05-\\u0C0C|\\u0C0E-\\u0C10|\\u0C12-\\u0C28|\\u0C2A-\\u0C33|\\u0C35-\\u0C39|" +
        "\\u0C60-\\u0C61|\\u0C85-\\u0C8C|\\u0C8E-\\u0C90|\\u0C92-\\u0CA8|\\u0CAA-\\u0CB3|\\u0CB5-\\u0CB9|\\u0CDE|" +
        "\u0CE0-\\u0CE1|\\u0D05-\\u0D0C|\\u0D0E-\\u0D10|\\u0D12-\\u0D28|\\u0D2A-\\u0D39|\\u0D60-\\u0D61|\\u0E01-\\u0E2E|" +
        "\\u0E30|\\u0E32-\\u0E33|\\u0E40-\\u0E45|\\u0E81-\\u0E82|\\u0E84|\\u0E87-\\u0E88|\\u0E8A|\\u0E8D|\\u0E94-\\u0E97|" +
        "\\u0E99-\\u0E9F|\\u0EA1-\\u0EA3|\\u0EA5|\\u0EA7|\\u0EAA-\\u0EAB|\\u0EAD-\\u0EAE|\\u0EB0|\\u0EB2-\\u0EB3|\\u0EBD|" +
        "\\u0EC0-\\u0EC4|\\u0F40-\\u0F47|\\u0F49-\\u0F69|\\u10A0-\\u10C5|\\u10D0-\\u10F6|\\u1100|\\u1102-\\u1103|" +
        "\\u1105-\\u1107|\\u1109|\\u110B-\\u110C|\\u110E-\\u1112|\\u113C|\\u113E|\\u1140|\\u114C|\\u114E|\\u1150|" +
        "\\u1154-\\u1155|\\u1159|\\u115F-\\u1161|\\u1163|\\u1165|\\u1167|\\u1169|\\u116D-\\u116E|\\u1172-\\u1173|\\u1175|" +
        "\\u119E|\\u11A8|\\u11AB|\\u11AE-\\u11AF|\\u11B7-\\u11B8|\\u11BA|\\u11BC-\\u11C2|\\u11EB|\\u11F0|\\u11F9|" +
        "\\u1E00-\\u1E9B|\\u1EA0-\\u1EF9|\\u1F00-\\u1F15|\\u1F18-\\u1F1D|\\u1F20-\\u1F45|\\u1F48-\\u1F4D|\\u1F50-\\u1F57|" +
        "\\u1F59|\\u1F5B|\\u1F5D|\\u1F5F-\\u1F7D|\\u1F80-\\u1FB4|\\u1FB6-\\u1FBC|\\u1FBE|\\u1FC2-\\u1FC4|\\u1FC6-\\u1FCC|" +
        "\\u1FD0-\\u1FD3|\\u1FD6-\\u1FDB|\\u1FE0-\\u1FEC|\\u1FF2-\\u1FF4|\\u1FF6-\\u1FFC|\\u2126|\\u212A-\\u212B|\\u212E|" +
        "\\u2180-\\u2182|\\u3041-\\u3094|\\u30A1-\\u30FA|\\u3105-\\u312C|\\uAC00-\\uD7A3";

    public static final String IDEOGRAPHIC = "\\u4E00-\\u9FA5|\\u3007|\\u3021-\\u3029";

    public static final String COMBINING_CHAR =
        "\\u0300-\\u0345|\\u0360-\\u0361|\\u0483-\\u0486|\\u0591-\\u05A1|\\u05A3-\\u05B9|\\u05BB-\\u05BD|\\u05BF|" +
        "\\u05C1-\\u05C2|\\u05C4|\\u064B-\\u0652|\\u0670|\\u06D6-\\u06DC|\\u06DD-\\u06DF|\\u06E0-\\u06E4|\\u06E7-\\u06E8|" +
        "\\u06EA-\\u06ED|\\u0901-\\u0903|\\u093C|\\u093E-\\u094C|\\u094D|\\u0951-\\u0954|\\u0962-\\u0963|\\u0981-\\u0983|" +
        "\\u09BC|\\u09BE|\\u09BF|\\u09C0-\\u09C4|\\u09C7-\\u09C8|\\u09CB-\\u09CD|\\u09D7|\\u09E2-\\u09E3|\\u0A02|\\u0A3C|" +
        "\\u0A3E|\\u0A3F|\\u0A40-\\u0A42|\\u0A47-\\u0A48|\\u0A4B-\\u0A4D|\\u0A70-\\u0A71|\\u0A81-\\u0A83|\\u0ABC|" +
        "\\u0ABE-\\u0AC5|\\u0AC7-\\u0AC9|\\u0ACB-\\u0ACD|\\u0B01-\\u0B03|\\u0B3C|\\u0B3E-\\u0B43|\\u0B47-\\u0B48|" +
        "\\u0B4B-\\u0B4D|\\u0B56-\\u0B57|\\u0B82-\\u0B83|\\u0BBE-\\u0BC2|\\u0BC6-\\u0BC8|\\u0BCA-\\u0BCD|\\u0BD7|" +
        "\\u0C01-\\u0C03|\\u0C3E-\\u0C44|\\u0C46-\\u0C48|\\u0C4A-\\u0C4D|\\u0C55-\\u0C56|\\u0C82-\\u0C83|\\u0CBE-\\u0CC4|" +
        "\\u0CC6-\\u0CC8|\\u0CCA-\\u0CCD|\\u0CD5-\\u0CD6|\\u0D02-\\u0D03|\\u0D3E-\\u0D43|\\u0D46-\\u0D48|\\u0D4A-\\u0D4D|" +
        "\\u0D57|\\u0E31|\\u0E34-\\u0E3A|\\u0E47-\\u0E4E|\\u0EB1|\\u0EB4-\\u0EB9|\\u0EBB-\\u0EBC|\\u0EC8-\\u0ECD|" +
        "\\u0F18-\\u0F19|\\u0F35|\\u0F37|\\u0F39|\\u0F3E|\\u0F3F|\\u0F71-\\u0F84|\\u0F86-\\u0F8B|\\u0F90-\\u0F95|\\u0F97|" +
        "\\u0F99-\\u0FAD|\\u0FB1-\\u0FB7|\\u0FB9|\\u20D0-\\u20DC|\\u20E1|\\u302A-\\u302F|\\u3099|\\u309A";

    public static final String DIGIT =
        "\\u0030-\\u0039|\\u0660-\\u0669|\\u06F0-\\u06F9|\\u0966-\\u096F|\\u09E6-\\u09EF|\\u0A66-\\u0A6F|\\u0AE6-\\u0AEF|" +
        "\\u0B66-\\u0B6F|\\u0BE7-\\u0BEF|\\u0C66-\\u0C6F|\\u0CE6-\\u0CEF|\\u0D66-\\u0D6F|\\u0E50-\\u0E59|\\u0ED0-\\u0ED9|" +
        "\\u0F20-\\u0F29";

    public static final String EXTENDER =
        "\\u00B7|\\u02D0|\\u02D1|\\u0387|\\u0640|\\u0E46|\\u0EC6|\\u3005|\\u3031-\\u3035|\\u309D-\\u309E|\\u30FC-\\u30FE";

    /*
    name's prefix must be a valid xml name:
    http://www.w3.org/TR/REC-xml-names
    [4] 	NCName 	 ::= 	(Letter | '_') (NCNameChar)*	  // An XML Name, minus the ":"
    [5] 	NCNameChar 	::= 	Letter | Digit | '.' | '-' | '_' | CombiningChar | Extender
    [84]   	Letter	   ::=   	BaseChar | Ideographic
    */

    public static final String LETTER = BASE_CHAR + "|" + IDEOGRAPHIC;

    public static final String NC_NAME =
        "[" + LETTER + "|_]" +
        "[" +  LETTER + "|" + DIGIT + "|.|\\-|_|" + COMBINING_CHAR + "|" + EXTENDER +"]*";

    public static final String SIMPLENAME_CHAR =
        "[^/:\\[\\]\\*'\"\\s]";

    public static final String PATTERNSTRING_NAME =
        "((" + NC_NAME + "):)?" + // prefix
        SIMPLENAME_CHAR + "([" + SIMPLENAME_CHAR + "| ]*" + SIMPLENAME_CHAR +")?";

    public static final Pattern PATTERN_NAME = Pattern.compile(PATTERNSTRING_NAME);

    public static final String PATTERNSTRING_PATH_ELEMENT =
       PATTERNSTRING_NAME +
       "(\\[[1-9]\\d*\\])?";

    public static final String PATTERNSTRING_PATH_WITHOUT_LAST_SLASH =
        "(\\./|\\.\\./|/)?" +
        "(" + PropertyUtil.PATTERNSTRING_PATH_ELEMENT + "/)*" +
        PropertyUtil.PATTERNSTRING_PATH_ELEMENT;

    public static final String PATTERNSTRING_PATH =
        PATTERNSTRING_PATH_WITHOUT_LAST_SLASH + "/?";
    public static final Pattern PATTERN_PATH = Pattern.compile(PATTERNSTRING_PATH);

    public static final String PATTERNSTRING_DATE =
        "[0-9][0-9][0-9][0-9]-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]||2[0-9]|3[01])" +
        "T([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9].[0-9][0-9][0-9]" +
        "(Z|[+-]([0-1][0-9]|2[0-3]):[0-5][0-9])";
    public static final Pattern PATTERN_DATE = Pattern.compile(PATTERNSTRING_DATE);

    /**
     * Private constructor to disable instantiation.
     */
    private PropertyUtil() {
    }

    /**
     * Traverses a tree below a given node searching for a property with a given
     * type
     *
     * @param node the node to start traverse
     * @param type the property type to search for
     * @param multiple whether the property should be multivalued (<code>null</code>: does not matter)
     * @return the property found or null if no property is found
     */
    public static Property searchProp(Session session, Node node, int type, Boolean multiple)
            throws RepositoryException, ValueFormatException {

        Property prop = null;
        int propType = PropertyType.UNDEFINED;
        if (prop == null) {
            for (PropertyIterator props = node.getProperties(); props.hasNext();) {
                Property property = props.nextProperty();
                propType = property.getType();
                if (propType == type && (multiple == null || multiple.booleanValue() == property.getDefinition().isMultiple())) {
                    prop = property;
                    break;
                }
            }
        }
        if (prop == null) {
            for (NodeIterator nodes = node.getNodes(); nodes.hasNext();) {
                Node n = nodes.nextNode();
                prop = searchProp(session, n, type, multiple);
                if (prop != null) {
                    break;
                }
            }
        }
        return prop;
    }

    /**
     * Returns the value of a property. If <code>prop</code> is multi valued
     * this method returns the first value.
     *
     * @param prop the property from which to return the value.
     * @return the value of the property.
     */
    public static Value getValue(Property prop) throws RepositoryException {
        Value val;
        if (prop.getDefinition().isMultiple()) {
            Value[] vals = prop.getValues();
            if (vals.length > 0) {
                val = vals[0];
            } else {
                val = null;
            }
        } else {
            val = prop.getValue();
        }
        return val;
    }

    /**
     * checks if the given name follows the NAME syntax rules and if a present
     * prefix is mapped to a registered namespace
     *
     * @param name the string to test
     */
    public static boolean checkNameFormat(String name, Session session) throws RepositoryException {
        if (name == null || name.length() == 0) {
            return false;
        } else {
            NamespaceRegistry nsr = session.getWorkspace().getNamespaceRegistry();
            boolean prefixOk = true;
            // validate name element
            Matcher matcher = PATTERN_NAME.matcher(name);
            // validate namespace prefixes if present
            String[] split = name.split(":");
            if (split.length > 1) {
                String prefix = split[0];
                try {
                    nsr.getURI(prefix);
                } catch (NamespaceException nse) {
                    prefixOk = false;
                }
            }
            return matcher.matches() && prefixOk;
        }
    }

    /**
     * Checks if the given path follows the path syntax rules.
     *
     * @param jcrPath the string to test
     */
    public static boolean checkPathFormat(String jcrPath, Session session) throws RepositoryException {
        if (jcrPath == null || jcrPath.length() == 0) {
            return false;
        } else if (jcrPath.equals("/")) {
            return true;
        } else {
            NamespaceRegistry nsr = session.getWorkspace().getNamespaceRegistry();
            boolean match = false;
            boolean prefixOk = true;
            // split path into path elements and validate each of them
            String[] elems = jcrPath.split("/", -1);
            for (int i = jcrPath.startsWith("/") ? 1 : 0; i < elems.length; i++) {
                // validate path element
                String elem = elems[i];
                Matcher matcher = PATTERN_PATH.matcher(elem);
                match = matcher.matches();
                if (!match) {
                    break;
                }
                // validate namespace prefixes if present
                String[] split = elem.split(":");
                if (split.length > 1) {
                    String prefix = split[0];
                    try {
                        nsr.getURI(prefix);
                    } catch (NamespaceException nse) {
                        prefixOk = false;
                        break;
                    }
                }
            }
            return match && prefixOk;
        }
    }

    /**
     * Checks if the String is a valid date in string format.
     *
     * @param str the string to test.
     * @return <code>true</code> if <code>str</code> is a valid date format.
     */
    public static boolean isDateFormat(String str) {
        return PATTERN_DATE.matcher(str).matches();
    }

    /**
     * Counts the number of bytes of a Binary value.
     *
     * @param val the binary value.
     * @return the number of bytes or -1 in case of any exception
     */
    public static long countBytes(Value val) {
        int length = 0;
        InputStream in = null;
        try {
            in = val.getStream();
            BufferedInputStream bin = new BufferedInputStream(in);
            while (bin.read() != -1) {
                length++;
            }
            bin.close();
        } catch (Exception e) {
            length = -1;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ignore) {}
            }
        }
        return length;
    }

    /**
     * Helper method to test the type received with Value.getType() and
     * Property.getType() .
     */
    public static boolean checkGetType(Property prop, int propType) throws RepositoryException {
        Value val = getValue(prop);
        boolean samePropType = (val.getType() == propType);
        int requiredType = prop.getDefinition().getRequiredType();
        if (requiredType != PropertyType.UNDEFINED) {
            samePropType = (val.getType() == requiredType);
        }
        return samePropType;
    }

    /**
     * Helper method to compare the equality of two values for equality with the
     * fulfilling of the equality conditions. These conditions for the values
     * are to have the same type and the same string representation.
     *
     * @param val1 first value
     * @param val2 second value
     * @return true if the equals method is equivalent to the normative
     *         definition of value equality, false in the other case.
     */
    public static boolean equalValues(Value val1, Value val2) throws RepositoryException {

        boolean isEqual = val1.equals(val2);
        boolean conditions = false;
        try {
            conditions = (val1.getType() == val2.getType())
                    && val1.getString().equals(val2.getString());
        } catch (ValueFormatException vfe) {
            return false;
        }
        return (isEqual == conditions);
    }

    /**
     * Helper method to assure that no property with a null value exist.
     *
     * @param node the node to start the search from.
     * @return <code>true</code> if a null value property is found;
     *         <code>false</code> in the other case.
     */
    public static boolean nullValues(Node node) throws RepositoryException {
        boolean nullValue = false;
        for (PropertyIterator props = node.getProperties(); props.hasNext();) {
            Property property = props.nextProperty();
            if (!property.getDefinition().isMultiple()) {
                nullValue = (property.getValue() == null);
                if (nullValue) {
                    break;
                }
            }
        }

        if (!nullValue) {
            for (NodeIterator nodes = node.getNodes(); nodes.hasNext();) {
                Node n = nodes.nextNode();
                nullValue = nullValues(n);
            }
        }
        return nullValue;
    }

    /**
     * Helper method to find a multivalue property.
     *
     * @param node the node to start the search from.
     * @return a multivalue property or null if not found any.
     */
    public static Property searchMultivalProp(Node node) throws RepositoryException {
        Property multiVal = null;
        for (PropertyIterator props = node.getProperties(); props.hasNext();) {
            Property property = props.nextProperty();
            if (property.getDefinition().isMultiple()) {
                multiVal = property;
                break;
            }
        }

        if (multiVal == null) {
            for (NodeIterator nodes = node.getNodes(); nodes.hasNext();) {
                Node n = nodes.nextNode();
                multiVal = searchMultivalProp(n);
                if (multiVal != null) {
                    break;
                }
            }
        }
        return multiVal;
    }

    /**
     * Helper method to find a multivalue property of a given type.
     *
     * @param node the node to start the search from.
     * @param type the property type.
     * @return a multivalue property or null if not found any.
     */
    public static Property searchMultivalProp(Node node, int type) throws RepositoryException {
        Property multiVal = null;
        for (PropertyIterator props = node.getProperties(); props.hasNext();) {
            Property property = props.nextProperty();
            if (property.getDefinition().isMultiple()
                    && property.getType() == type) {
                multiVal = property;
                break;
            }
        }

        if (multiVal == null) {
            for (NodeIterator nodes = node.getNodes(); nodes.hasNext();) {
                Node n = nodes.nextNode();
                multiVal = searchMultivalProp(n, type);
                if (multiVal != null) {
                    break;
                }
            }
        }
        return multiVal;
    }

    /**
     * Retrieve a single valued property from the given node.
     *
     * @param node
     * @return the property found or null if no property is found.
     */
    public static Property searchSingleValuedProperty(Node node)
            throws RepositoryException, ValueFormatException {
        PropertyIterator props = node.getProperties();
        while (props.hasNext()) {
            Property p = props.nextProperty();
            if (!p.getDefinition().isMultiple()) {
                return p;
            }
        }
        // should never get here, since every Node must provide the jcr:primaryType
        // property, which is single valued.
        return null;
    }
}

