// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.madvoc.proxetta;

import jodd.cache.TypeCache;
import jodd.madvoc.component.ActionsManager;
import jodd.madvoc.config.ActionDefinition;
import jodd.madvoc.config.ActionRuntime;
import jodd.petite.meta.PetiteInject;
import jodd.proxetta.Proxetta;

import java.lang.reflect.Method;

/**
 * Madvoc {@link jodd.madvoc.component.ActionsManager actions manager} that works with Proxetta.
 */
public class ProxettaAwareActionsManager extends ActionsManager {

	@PetiteInject
	protected ProxettaSupplier proxettaSupplier;

	protected final TypeCache<Class> proxyActionClasses;

	public ProxettaAwareActionsManager() {
		this.proxyActionClasses = TypeCache.createDefault();
	}

	/**
	 * Registers actions and applies proxetta on actions that are not already registered.
	 * We need to define {@link ActionDefinition} before we apply the proxy, using
	 * target action class.
	 */
	@Override
	public synchronized ActionRuntime registerAction(Class actionClass, final Method actionMethod, ActionDefinition actionDefinition) {
		if (proxettaSupplier == null) {
			return super.registerAction(actionClass, actionMethod, actionDefinition);
		}

		if (actionDefinition == null) {
			actionDefinition = actionMethodParser.parseActionDefinition(actionClass, actionMethod);
		}

		// create proxy for action class if not already created

		Class existing = proxyActionClasses.get(actionClass);

		if (existing == null) {
			final Proxetta proxetta = proxettaSupplier.get();

			existing = proxetta.proxy().setTarget(actionClass).define();

			proxyActionClasses.put(actionClass, existing);
		}

		actionClass = existing;

		return super.registerAction(actionClass, actionMethod, actionDefinition);
	}
}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.typeconverter.impl;

import jodd.typeconverter.TypeConverter;
import jodd.typeconverter.TypeConverterManager;
import jodd.util.CsvUtil;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Converts given object to an array. This converter is specific, as it
 * is not directly registered to a type; but created when needed.
 * Conversion rules:
 *

 *
null value is returned as null

 *
source non-array value is checked for Collections

 *
if non-array element can't be resolved, it is converted to single element array

 *
source array is converted to target array, by converting each element

 *

 */
@SuppressWarnings("unchecked")
public class ArrayConverter<T> implements TypeConverter<T[]> {

	public static final char[] NUMBER_DELIMITERS = new char[] {',', ';', '\n'};

	protected final TypeConverterManager typeConverterManager;
	protected final Class<T> targetComponentType;

	public ArrayConverter(final TypeConverterManager typeConverterManager, final Class<T> targetComponentType) {
		this.typeConverterManager = typeConverterManager;
		this.targetComponentType = targetComponentType;
	}

	@Override
	public T[] convert(final Object value) {
		if (value == null) {
			return null;
		}

		Class valueClass = value.getClass();

		if (!valueClass.isArray()) {
			// source is not an array
	        return convertValueToArray(value);
		}

		// source is an array
		return convertArrayToArray(value);
	}

	/**
	 * Converts type using type converter manager.
	 */
	protected T convertType(final Object value) {
		return typeConverterManager.convertType(value, targetComponentType);
	}

	/**
	 * Creates new array of target component type.
	 * Default implementation uses reflection to create
	 * an array of target type. Override it for better performances.
	 */
	protected T[] createArray(final int length) {
		return (T[]) Array.newInstance(targetComponentType, length);
	}

	/**
	 * Creates an array with single element.
	 */
	protected T[] convertToSingleElementArray(final Object value) {
		T[] singleElementArray = createArray(1);

		singleElementArray[0] = convertType(value);

		return singleElementArray;
	}

	/**
	 * Converts non-array value to array. Detects various
	 * collection types and iterates them to make conversion
	 * and to create target array.
 	 */
	protected T[] convertValueToArray(final Object value) {
		if (value instanceof Collection) {
			Collection collection = (Collection) value;
			T[] target = createArray(collection.size());

			int i = 0;
			for (Object element : collection) {
				target[i] = convertType(element);
				i++;
			}

			return target;
		}

		if (value instanceof Iterable) {
			Iterable iterable = (Iterable) value;
			List<T> list = new ArrayList<>();

			for (Object element : iterable) {
				list.add(convertType(element));
			}

			T[] target = createArray(list.size());
			return list.toArray(target);
		}

		if (value instanceof CharSequence) {
			String[] strings = convertStringToArray(value.toString());
			return convertArrayToArray(strings);
		}

		// everything else:
		return convertToSingleElementArray(value);
	}

	/**
	 * Converts string to array, for the {@link #convertValueToArray(Object)} method.
	 * By default, the string is converted into an array using {@link jodd.util.CsvUtil}.
	 */
	protected String[] convertStringToArray(final String value) {
		return CsvUtil.toStringArray(value);
	}

	/**
	 * Converts array value to array.
	 */
	protected T[] convertArrayToArray(final Object value) {
		Class valueComponentType = value.getClass().getComponentType();

		if (valueComponentType == targetComponentType) {
			// equal types, no conversion needed
			return (T[]) value;
		}

		T[] result;

		if (valueComponentType.isPrimitive()) {
			// convert primitive array to target array
			result = convertPrimitiveArrayToArray(value, valueComponentType);
		} else {
			// convert object array to target array
			Object[] array = (Object[]) value;
			result = createArray(array.length);

			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}

		return result;
	}

	/**
	 * Converts primitive array to target array.
	 */
	@SuppressWarnings("AutoBoxing")
	protected T[] convertPrimitiveArrayToArray(final Object value, final Class primitiveComponentType) {
		T[] result = null;

		if (primitiveComponentType == int.class) {
			int[] array = (int[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		else if (primitiveComponentType == long.class) {
			long[] array = (long[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		else if (primitiveComponentType == float.class) {
			float[] array = (float[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		else if (primitiveComponentType == double.class) {
			double[] array = (double[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		else if (primitiveComponentType == short.class) {
			short[] array = (short[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		else if (primitiveComponentType == byte.class) {
			byte[] array = (byte[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		else if (primitiveComponentType == char.class) {
			char[] array = (char[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		else if (primitiveComponentType == boolean.class) {
			boolean[] array = (boolean[]) value;
			result = createArray(array.length);
			for (int i = 0; i < array.length; i++) {
				result[i] = convertType(array[i]);
			}
		}
		return result;
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.proxetta;

import jodd.asm.TraceSignatureVisitor;
import jodd.asm7.signature.SignatureReader;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Collector of generics information from the signature.
 */
public class GenericsReader {

	/**
	 * Parses signature for generic information and returns a map where key is generic name
	 * and value is raw type. Returns an empty map if signature does not define any generics.
	 */
	public Map<String, String> parseSignatureForGenerics(final String signature, final boolean isInterface) {

		if (signature == null) {
			return Collections.emptyMap();
		}

		final Map<String, String> genericsMap = new HashMap<>();

		SignatureReader sr = new SignatureReader(signature);
		StringBuilder sb = new StringBuilder();
		TraceSignatureVisitor v = new TraceSignatureVisitor(sb, isInterface) {
			String genericName;

			@Override
			public void visitFormalTypeParameter(final String name) {
				genericName = name;
				super.visitFormalTypeParameter(name);
			}

			@Override
			public void visitClassType(final String name) {
				if (genericName != null) {
					genericsMap.put(genericName, 'L' + name + ';');
					genericName = null;
				}
				super.visitClassType(name);
			}

		};
		sr.accept(v);

		return genericsMap;
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.petite.resolver;

import jodd.introspector.ClassDescriptor;
import jodd.introspector.ClassIntrospector;
import jodd.introspector.FieldDescriptor;
import jodd.introspector.MethodDescriptor;
import jodd.introspector.PropertyDescriptor;
import jodd.petite.def.SetInjectionPoint;
import jodd.petite.meta.PetiteInject;
import jodd.util.ClassUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Resolves collection fields.
 */
public class SetResolver {

	/**
	 * Resolves all collections for given type.
	 */
	public SetInjectionPoint[] resolve(final Class type, final boolean autowire) {
		ClassDescriptor cd = ClassIntrospector.get().lookup(type);
		List<SetInjectionPoint> list = new ArrayList<>();

		PropertyDescriptor[] allProperties = cd.getAllPropertyDescriptors();

		for (PropertyDescriptor propertyDescriptor : allProperties) {

			if (propertyDescriptor.isGetterOnly()) {
				continue;
			}

			Class propertyType = propertyDescriptor.getType();
			if (!ClassUtil.isTypeOf(propertyType, Collection.class)) {
				continue;
			}

			MethodDescriptor writeMethodDescriptor = propertyDescriptor.getWriteMethodDescriptor();
			FieldDescriptor fieldDescriptor = propertyDescriptor.getFieldDescriptor();

			PetiteInject ref = null;

			if (writeMethodDescriptor != null) {
				ref = writeMethodDescriptor.getMethod().getAnnotation(PetiteInject.class);
			}
			if (ref == null && fieldDescriptor != null) {
				ref = fieldDescriptor.getField().getAnnotation(PetiteInject.class);
			}

			if ((!autowire) && (ref == null)) {
				continue;
			}

			list.add(new SetInjectionPoint(propertyDescriptor));
		}

		SetInjectionPoint[] fields;

		if (list.isEmpty()) {
			fields = SetInjectionPoint.EMPTY;
		} else {
			fields = list.toArray(new SetInjectionPoint[0]);
		}
		return fields;
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.json;

import jodd.introspector.ClassDescriptor;
import jodd.introspector.ClassIntrospector;
import jodd.util.ClassUtil;
import jodd.util.Wildcard;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import static jodd.util.StringPool.NULL;

/**
 * JSON context used during serialization for building the JSON string.
 */
public class JsonContext extends JsonWriter {

	// ---------------------------------------------------------------- ctor

	protected final JsonSerializer jsonSerializer;
	protected final List<JsonValueContext> bag;
	protected int bagSize = 0;
	protected final Path path;
	protected final boolean excludeNulls;
	protected final boolean excludeEmpty;
	protected final Function<Object, TypeJsonSerializer> serializerResolver;

	public JsonContext(final JsonSerializer jsonSerializer, final Appendable appendable) {
		super(appendable, jsonSerializer.strictStringEncoding);
		this.jsonSerializer = jsonSerializer;
		this.bag = new ArrayList<>();
		this.path = new Path();
		this.excludeNulls = jsonSerializer.excludeNulls;
		this.excludeEmpty = jsonSerializer.excludeEmpty;
		this.serializerResolver = jsonSerializer.serializerResolver;
	}

	/**
	 * Returns {@link jodd.json.JsonSerializer}.
	 */
	public JsonSerializer getJsonSerializer() {
		return jsonSerializer;
	}

	/**
	 * Returns true if null values have to be excluded.
	 */
	public boolean isExcludeNulls() {
		return excludeNulls;
	}

	public boolean isExcludeEmpty() {
		return excludeEmpty;
	}

	// ---------------------------------------------------------------- path and value context

	protected JsonValueContext lastValueContext = null;

	/**
	 * Returns true if object has been already processed during the serialization.
	 * Used to prevent circular dependencies. Objects are matched by identity.
	 */
	public boolean pushValue(final Object value) {
		for (int i = 0; i < bagSize; i++) {
			JsonValueContext valueContext = bag.get(i);
			if (valueContext.getValue() == value) {
				return true;
			}
		}

		if (bagSize == bag.size()) {
			lastValueContext = new JsonValueContext(value);
			bag.add(lastValueContext);
		}
		else {
			lastValueContext = bag.get(bagSize);
			lastValueContext.reuse(value);
		}

		bagSize++;

		return false;
	}

	/**
	 * Removes object from current bag, indicating it is not anymore in the path.
	 */
	public void popValue() {
		bagSize--;
		if (bagSize == 0) {
			lastValueContext = null;
		} else {
			lastValueContext = bag.get(bagSize - 1);
		}
	}

	/**
	 * Returns current {@link jodd.json.JsonValueContext value context}.
	 * It may be null if value is not {@link #pushValue(Object) pushed} yet.
	 */
	public JsonValueContext peekValueContext() {
		return lastValueContext;
	}

	/**
	 * Returns current path.
	 */
	public Path getPath() {
		return path;
	}

	// ---------------------------------------------------------------- overwrite

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void pushName(final String name, final boolean withComma) {
		JsonValueContext valueContext = peekValueContext();

		if (valueContext != null) {
			valueContext.setPropertyName(name);
		}

		super.pushName(name, withComma);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void writeComma() {
		JsonValueContext valueContext = peekValueContext();

		if (valueContext != null) {
			valueContext.incrementIndex();
		}

		super.writeComma();
	}

	// ---------------------------------------------------------------- serializer

	/**
	 * Serializes the object using {@link jodd.json.TypeJsonSerializer type serializer}.
	 * Returns true if object was written, otherwise returns false.
	 */
	public boolean serialize(final Object object) {
		if (object == null) {
			write(NULL);

			return true;
		}

		TypeJsonSerializer typeJsonSerializer = null;

		// callback

		if (serializerResolver != null) {
			typeJsonSerializer = serializerResolver.apply(object);
		}

		if (typeJsonSerializer == null) {

			// + read paths map

			if (jsonSerializer.pathSerializersMap != null) {
				typeJsonSerializer = jsonSerializer.pathSerializersMap.get(path);
			}

			final Class type = object.getClass();

			// + read local types map

			if (jsonSerializer.typeSerializersMap != null) {
				typeJsonSerializer = jsonSerializer.typeSerializersMap.lookup(type);
			}

			// + globals

			if (typeJsonSerializer == null) {
				typeJsonSerializer = TypeJsonSerializerMap.get().lookup(type);
			}
		}

		return typeJsonSerializer.serialize(this, object);
	}

	// ---------------------------------------------------------------- matchers

	/**
	 * Matches property types that are ignored by default.
	 */
	public boolean matchIgnoredPropertyTypes(final Class propertyType, final boolean excludeMaps, final boolean include) {
		if (!include) {
			return false;
		}

		if (propertyType != null) {
			if (!jsonSerializer.deep) {
				ClassDescriptor propertyTypeClassDescriptor = ClassIntrospector.get().lookup(propertyType);

				if (propertyTypeClassDescriptor.isArray()) {
					return false;
				}
				if (propertyTypeClassDescriptor.isCollection()) {
					return false;
				}
				if (excludeMaps) {
					if (propertyTypeClassDescriptor.isMap()) {
						return false;
					}
				}
			}

			// still not excluded, continue with excluded types and type names

			// + excluded types

			if (jsonSerializer.excludedTypes != null) {
				for (Class excludedType : jsonSerializer.excludedTypes) {
					if (ClassUtil.isTypeOf(propertyType, excludedType)) {
						return false;
					}
				}
			}

			// + exclude type names

			final String propertyTypeName = propertyType.getName();

			if (jsonSerializer.excludedTypeNames != null) {
				for (String excludedTypeName : jsonSerializer.excludedTypeNames) {
					if (Wildcard.match(propertyTypeName, excludedTypeName)) {
						return false;
					}
				}
			}
		}

		return true;
	}

	/**
	 * Matched current path to queries. If match is found, provided include
	 * value may be changed.
	 */
	public boolean matchPathToQueries(final boolean include) {
		return jsonSerializer.rules.apply(path, include);
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.db;

import jodd.db.connection.ConnectionProvider;
import jodd.log.Logger;
import jodd.log.LoggerFactory;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;

/**
 * Encapsulates db connection. Initially works in auto-commit mode.
 * May start and work with transactions, after committing/rolling back
 * DbSession goes back to auto-commit mode.
 *

 * All invoked queries are stored within one session and closed implicitly
 * on session closing.
 *

 * For managed transaction see DbJtxTransactionManager from jodd-tx.
 */
public class DbSession implements AutoCloseable {

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

	// ---------------------------------------------------------------- init & close

	protected final ConnectionProvider connectionProvider;
	protected Connection connection;

	/**
	 * Creates new database session with default transaction mode and in autocommit mode.
	 */
	public DbSession(ConnectionProvider connectionProvider) {
		log.debug("Creating new db session");

		if (connectionProvider == null) {
			connectionProvider = DbOom.get().connectionProvider();

			if (connectionProvider == null) {
				throw new DbSqlException("Connection provider is not available");
			}
		}

		this.connectionProvider = connectionProvider;
		this.txActive = false;
		this.txMode = DbTransactionMode.READ_ONLY_TX;
		this.queries = new HashSet<>();
	}

	/**
	 * Closes current session and all allocated resources.
	 * All attached queries are closed. If a transaction is still active, exception occurs.
	 * Database connection is returned to the {@link ConnectionProvider}.
	 * Closed session is no longer available for usage.
	 */
	public void closeSession() {
		log.debug("Closing db session");

		SQLException sqlException = null;

		if (queries != null) {
			for (DbQueryBase query : queries) {
				SQLException sex = query.closeQuery();
				if (sex != null) {
					if (sqlException == null) {
						sqlException = sex;
					} else {
						sqlException.setNextException(sex);
					}
				}
			}
		}
		if (connection != null) {
			if (txActive) {
				throw new DbSqlException("TX was not closed before closing the session");
			}
			connectionProvider.closeConnection(connection);
			connection = null;
		}
		queries = null;
		if (sqlException != null) {
			throw new DbSqlException("Closing DbSession failed", sqlException);
		}
	}

	@Override
	public void close() {
		closeSession();
	}

	/**
	 * Indicates whether a session is closed.
	 */
	public boolean isSessionClosed() {
		return queries == null;
	}

	/**
	 * Returns true if session is open.
	 */
	public boolean isSessionOpen() {
		return queries != null;
	}

	// ---------------------------------------------------------------- query

	/**
	 * Bag of all queries attached to this session. Explicitly closed queries
	 * remains in the set. If null, session is closed.
	 * If not null, but empty, session is still considered as open.
	 */
	protected Set<DbQueryBase> queries;

	/**
	 * Returns total number of queries assigned to this session.
	 */
	public int getTotalQueries() {
		if (queries == null) {
			return 0;
		}
		return queries.size();
	}

	/**
	 * Returns current connection.
	 */
	public Connection getConnection() {
		return connection;
	}

	/**
	 * Attaches a new {@link DbQuery}. May be invoked both inside and outside of transaction.
	 */
	protected void attachQuery(final DbQueryBase query) {
		assertSessionIsOpen();
		openConnectionForQuery();
		queries.add(query);
	}

	/**
	 * Detach used {@link DbQuery}. Usually invoked by {@link jodd.db.DbQuery#close()}.
	 */
	protected void detachQuery(final DbQueryBase query) {
		queries.remove(query);
	}

	/**
	 * Opens connection in auto-commit mode, if already not opened.
	 */
	protected void openConnectionForQuery() {
		if (connection == null) {
			connection = connectionProvider.getConnection();
			txActive = false;	// txAction should already be false
			try {
				connection.setAutoCommit(true);
			} catch (SQLException sex) {
				throw new DbSqlException("Failed to open non-TX connection", sex);
			}
		}
	}

	// ---------------------------------------------------------------- transaction

	protected boolean txActive;

	protected DbTransactionMode txMode;

	/**
	 * Indicate whether a transaction is in progress.
	 */
	public boolean isTransactionActive() {
		return txActive;
	}

	/**
	 * Opens a transaction.
	 */
	protected void openTx() {
		if (connection == null) {
			connection = connectionProvider.getConnection();
		}
		txActive = true;
		try {
			connection.setAutoCommit(false);
			if (txMode.getIsolation() != DbTransactionMode.ISOLATION_DEFAULT) {
				connection.setTransactionIsolation(txMode.getIsolation());
			}
			connection.setReadOnly(txMode.isReadOnly());
		} catch (SQLException sex) {
			throw new DbSqlException("Open TX failed", sex);
		}
	}

	/**
	 * Closes current transaction.
	 */
	protected void closeTx() {
		txActive = false;
		try {
			connection.setAutoCommit(true);
		} catch (SQLException sex) {
			throw new DbSqlException("Close TX failed", sex);
		}
	}

	/**
	 * Starts a transaction.
	 */
	public void beginTransaction(final DbTransactionMode mode) {
		log.debug("Beginning transaction");

		assertTxIsClosed();
		this.txMode = mode;
		openTx();
	}

	/**
	 * Commit the current transaction, writing any unflushed changes to the database.
	 * Transaction mode is closed.
	 */
	public void commitTransaction() {
		log.debug("Committing transaction");

		assertTxIsActive();
		try {
			connection.commit();
		} catch (SQLException sex) {
			throw new DbSqlException("Commit TX failed", sex);
		} finally {
			closeTx();
		}
	}

	/**
	 * Roll back the current transaction. Transaction mode is closed.
	 */
	public void rollbackTransaction() {
		log.debug("Rolling-back transaction");

		assertTxIsActive();

		try {
			connection.rollback();
		} catch (SQLException sex) {
			throw new DbSqlException("Rollback TX failed", sex);
		} finally {
			closeTx();
		}
	}

	// ---------------------------------------------------------------- checking

	protected void assertSessionIsOpen() {
		if (queries == null) {
			throw new DbSqlException("Session is closed");
		}
	}

	protected void assertTxIsClosed() {
		assertSessionIsOpen();
		if (txActive) {
			throw new DbSqlException("TX already started for this session");
		}
	}

	protected void assertTxIsActive() {
		assertSessionIsOpen();
		if (!txActive) {
			throw new DbSqlException("TX not available for this session");
		}
	}
}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.madvoc.path;

import jodd.madvoc.config.ActionDefinition;
import jodd.madvoc.config.ActionNames;
import jodd.util.StringPool;
import jodd.util.StringUtil;

/**
 * Common base for {@link jodd.madvoc.path.ActionNamingStrategy} implementations.
 */
public abstract class BaseNamingStrategy implements ActionNamingStrategy {

	protected static final String PACKAGE_MACRO = "{:package}";
	protected static final String CLASS_MACRO = "{:class}";
	protected static final String METHOD_MACRO = "{:name}";
	protected static final String HTTPMETHOD_MACRO = "{:method}";

	/**
	 * Replaces action path macros in the path.
	 * If one of the provided paths is null
	 * it will not be replaced - so to emphasize the problem.
	 */
	protected String replaceActionNameMacros(String path, final ActionNames actionNames) {
		final String packageName = actionNames.packageName();
		final String className = actionNames.className();
		final String methodName = actionNames.methodName();
		final String httpMethod = actionNames.httpMethod();

		if (packageName != null) {
			path = StringUtil.replace(path, PACKAGE_MACRO, packageName);
		}
		if (className != null) {
			path = StringUtil.replace(path, CLASS_MACRO, className);
		}
		if (methodName != null) {
			path = StringUtil.replace(path, METHOD_MACRO, methodName);
		}
		if (httpMethod != null) {
			path = StringUtil.replace(path, HTTPMETHOD_MACRO, httpMethod);
		}

		return path;
	}

	/**
	 * Single point of {@link ActionDefinition} creation.
	 * Also performs the replacement of action path macros!
	 */
	protected ActionDefinition createActionDef(String path, String httpMethod, String resultBasePath, final ActionNames actionNames) {
		path = replaceActionNameMacros(path, actionNames);

		if (httpMethod != null) {
			httpMethod = replaceActionNameMacros(httpMethod, actionNames);
		}

		if (resultBasePath != null) {
			resultBasePath = replaceActionNameMacros(resultBasePath, actionNames);
		}

		return new ActionDefinition(path, httpMethod, resultBasePath);
	}

	/**
	 * Returns {@code true} if path is absolute.
	 */
	protected boolean isAbsolutePath(final String path) {
		if (path == null) {
			return false;
		}
		return path.startsWith(StringPool.SLASH);
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.util;

import jodd.exception.UncheckedException;
import jodd.io.StreamUtil;
import jodd.io.findfile.ClassScanner;
import jodd.template.StringTemplateParser;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;

/**
 * Misc java.util.Properties utils.
 */
public class PropertiesUtil {

	// ---------------------------------------------------------------- to/from files

	/**
	 * Create properties from the file.
	 *
	 * @param fileName properties file name to load
	 */
	public static Properties createFromFile(final String fileName) throws IOException {
		return createFromFile(new File(fileName));
	}

	/**
	 * Create properties from the file.
	 *
	 * @param file properties file to load
	 */
	public static Properties createFromFile(final File file) throws IOException {
		Properties prop = new Properties();
		loadFromFile(prop, file);
		return prop;
	}

	/**
	 * Loads properties from the file. Properties are appended to the existing
	 * properties object.
	 *
	 * @param p        properties to fill in
	 * @param fileName properties file name to load
	 */
	public static void loadFromFile(final Properties p, final String fileName) throws IOException {
		loadFromFile(p, new File(fileName));
	}

	/**
	 * Loads properties from the file. Properties are appended to the existing
	 * properties object.
	 *
	 * @param p      properties to fill in
	 * @param file   file to read properties from
	 */
	public static void loadFromFile(final Properties p, final File file) throws IOException {
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			p.load(fis);
		} finally {
			StreamUtil.close(fis);
		}
	}

	/**
	 * Writes properties to a file.
	 *
	 * @param p        properties to write to file
	 * @param fileName destination file name
	 */
	public static void writeToFile(final Properties p, final String fileName) throws IOException {
		writeToFile(p, new File(fileName), null);
	}

	/**
	 * Writes properties to a file.
	 *
	 * @param p        properties to write to file
	 * @param fileName destination file name
	 * @param header   optional header
	 */
	public static void writeToFile(final Properties p, final String fileName, final String header) throws IOException {
		writeToFile(p, new File(fileName), header);
	}

	/**
	 * Writes properties to a file.
	 *
	 * @param p      properties to write to file
	 * @param file   destination file
	 */
	public static void writeToFile(final Properties p, final File file) throws IOException {
		writeToFile(p, file, null);
	}

	/**
	 * Writes properties to a file.
	 *
	 * @param p      properties to write to file
	 * @param file   destination file
	 * @param header optional header
	 */
	public static void writeToFile(final Properties p, final File file, final String header) throws IOException {
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(file);
			p.store(fos, header);
		} finally {
			StreamUtil.close(fos);
		}
	}

	// ---------------------------------------------------------------- to/from string

	/**
	 * Creates properties from string.
	 */
	public static Properties createFromString(final String data) throws IOException {
		Properties p = new Properties();
		loadFromString(p, data);
		return p;
	}

	/**
	 * Loads properties from string.
	 */
	public static void loadFromString(final Properties p, final String data) throws IOException {
		try (ByteArrayInputStream is = new ByteArrayInputStream(data.getBytes(StringPool.ISO_8859_1))) {
			p.load(is);
		}
	}

	// ---------------------------------------------------------------- subsets

	/**
	 * Creates new Properties object from the original one, by copying
	 * those properties that have specified first part of the key name.
	 * Prefix may be optionally stripped during this process.
	 *
	 * @param p         source properties, from which new object will be created
	 * @param prefix    key names prefix
	 *
	 * @return subset properties
	 */
	public static Properties subset(final Properties p, String prefix, final boolean stripPrefix) {
		if (StringUtil.isBlank(prefix)) {
			return p;
		}
		if (!prefix.endsWith(StringPool.DOT)) {
			prefix += '.';
		}
		Properties result = new Properties();
		int baseLen = prefix.length();
		for (Object o : p.keySet()) {
			String key = (String) o;
			if (key.startsWith(prefix)) {
				result.setProperty(stripPrefix ? key.substring(baseLen) : key, p.getProperty(key));
			}
		}
		return result;
	}

	// ---------------------------------------------------------------- load from classpath

	/**
	 * Creates properties from classpath.
	 */
	public static Properties createFromClasspath(final String... rootTemplate) {
		Properties p = new Properties();
		return loadFromClasspath(p, rootTemplate);
	}

	/**
	 * Loads properties from classpath file(s). Properties are specified using wildcards.
	 */
	public static Properties loadFromClasspath(final Properties p, final String... rootTemplate) {
			ClassScanner.create()
				.registerEntryConsumer(entryData -> UncheckedException.runAndWrapException(() -> p.load(entryData.openInputStream())))
				.includeResources(true)
				.ignoreException(true)
				.excludeAllEntries(true)
				.includeEntries(rootTemplate)
				.scanDefaultClasspath();
		return p;
	}

	// ---------------------------------------------------------------- variables

	/**
	 * Returns String property from a map. If key is not found, or if value is not a String, returns null.
	 * Mimics Property.getProperty but on map.
	 */
	public static String getProperty(final Map map, final String key) {
		return getProperty(map, key, null);
	}

	/**
	 * Returns String property from a map.
	 * @see #getProperty(java.util.Map, String)
	 */
	public static String getProperty(final Map map, final String key, final String defaultValue) {
		Object val = map.get(key);
		return (val instanceof String) ? (String) val : defaultValue;
	}

	/**
	 * Resolves all variables.
	 */
	public static void resolveAllVariables(final Properties prop) {
		for (Object o : prop.keySet()) {
			String key = (String) o;
			String value = resolveProperty(prop, key);
			prop.setProperty(key, value);
		}
	}

	private static final StringTemplateParser stp;

	static {
		stp = new StringTemplateParser();
		stp.setParseValues(true);
	}

	/**
	 * Returns property with resolved variables.
	 */
	public static String resolveProperty(final Map map, final String key) {
		String value = getProperty(map, key);
		if (value == null) {
			return null;
		}
		value = stp.parse(value, macroName -> getProperty(map, macroName));

		return value;
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.jtx.proxy;

import jodd.jtx.JtxTransactionMode;
import jodd.jtx.meta.ReadOnlyTransaction;
import jodd.jtx.meta.ReadWriteTransaction;
import jodd.jtx.meta.Transaction;
import jodd.jtx.meta.TransactionAnnotationValues;
import jodd.jtx.worker.LeanJtxWorker;
import jodd.proxetta.ProxettaException;
import jodd.util.StringUtil;
import jodd.util.annotation.AnnotationParser;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * Manager for {@link jodd.jtx.proxy.AnnotationTxAdvice}.
 */
public class AnnotationTxAdviceManager {

	protected static final String JTXCTX_PATTERN_CLASS = "$class";
	protected static final String JTXCTX_PATTERN_METHOD = "$method";

	protected final Map<String, JtxTransactionMode> txmap = new HashMap<>();

	protected final LeanJtxWorker jtxWorker;

	protected final JtxTransactionMode defaultTransactionMode;

	protected final String scopePattern;

	protected Class<? extends Annotation>[] annotations;
	protected AnnotationParser[] annotationParsers;

	// ---------------------------------------------------------------- ctors

	public AnnotationTxAdviceManager(final LeanJtxWorker jtxWorker) {
		this(jtxWorker, JTXCTX_PATTERN_CLASS + '#' + JTXCTX_PATTERN_METHOD, null);
	}

	public AnnotationTxAdviceManager(final LeanJtxWorker jtxWorker, final String scopePattern) {
		this(jtxWorker, scopePattern, null);
	}

	@SuppressWarnings( {"unchecked"})
	public AnnotationTxAdviceManager(final LeanJtxWorker jtxWorker, final String scopePattern, final JtxTransactionMode defaultTxMode) {
		this.jtxWorker = jtxWorker;
		this.defaultTransactionMode = defaultTxMode == null ? JtxTransactionMode.PROPAGATION_SUPPORTS_READ_ONLY : defaultTxMode;
		this.scopePattern = scopePattern;

		registerAnnotations(new Class[] {
			Transaction.class, ReadWriteTransaction.class, ReadOnlyTransaction.class
		});
	}

	// ---------------------------------------------------------------- methods

	/**
	 * Returns tx worker.
	 */
	public LeanJtxWorker getJtxWorker() {
		return jtxWorker;
	}

	/**
	 * Returns default transaction mode.
	 */
	public JtxTransactionMode getDefaultTransactionMode() {
		return defaultTransactionMode;
	}

	/**
	 * Resolves tx scope from scope pattern.
	 */
	public String resolveScope(final Class type, final String methodName) {
		if (scopePattern == null) {
			return null;
		}
		String ctx = scopePattern;
		ctx = StringUtil.replace(ctx, JTXCTX_PATTERN_CLASS, type.getName());
		ctx = StringUtil.replace(ctx, JTXCTX_PATTERN_METHOD, methodName);
		return ctx;
	}

	/**
	 * Reads transaction mode from method annotation. Annotations are cached for better performances.
	 * @param type target class
	 * @param methodName target method name over which the transaction should be wrapped
	 * @param methodArgTypes types of arguments, used to find the method
	 * @param unique unique method fingerprint that contains return and arguments type information
	 */
	public synchronized JtxTransactionMode getTxMode(final Class type, final String methodName, final Class[] methodArgTypes, final String unique) {
		String signature = type.getName() + '#' + methodName + '%' + unique;
		JtxTransactionMode txMode = txmap.get(signature);
		if (txMode == null) {
			if (!txmap.containsKey(signature)) {

				final Method m;
				try {
					m = type.getMethod(methodName, methodArgTypes);
				} catch (NoSuchMethodException nsmex) {
					throw new ProxettaException(nsmex);
				}

				final TransactionAnnotationValues txAnn = readTransactionAnnotation(m);
				if (txAnn != null) {
					txMode = new JtxTransactionMode(
						txAnn.propagation(),
						txAnn.isolation(),
						txAnn.readOnly(),
						txAnn.timeout()
					);
				} else {
					txMode = defaultTransactionMode;
				}
				txmap.put(signature, txMode);
			}
		}
		return txMode;
	}

	// ---------------------------------------------------------------- tx annotations

	/**
	 * Returns current TX annotations.
	 */
	public Class<? extends Annotation>[] getAnnotations() {
		return annotations;
	}

	/**
	 * Registers new TX annotations.
	 */
	@SuppressWarnings( {"unchecked"})
	public void registerAnnotations(final Class<? extends Annotation>[] annotations) {
		this.annotations = annotations;

		this.annotationParsers = new AnnotationParser[annotations.length];

		for (int i = 0; i < annotations.length; i++) {
			annotationParsers[i] = TransactionAnnotationValues.parserFor(annotations[i]);
		}
	}

	/**
	 * Finds TX annotation.
	 */
	protected TransactionAnnotationValues readTransactionAnnotation(final Method method) {
		for (AnnotationParser annotationParser : annotationParsers) {
			TransactionAnnotationValues tad = TransactionAnnotationValues.of(annotationParser, method);
			if (tad != null) {
				return tad;
			}
		}
		return null;
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.csselly;

import jodd.csselly.selector.AttributeSelector;
import jodd.csselly.selector.PseudoClassSelector;
import jodd.csselly.selector.PseudoFunctionSelector;
import jodd.lagarto.dom.Node;
import jodd.lagarto.dom.NodeFilter;
import jodd.util.StringPool;
import jodd.util.StringUtil;

import java.util.ArrayList;
import java.util.List;

import static jodd.csselly.selector.Match.EQUALS;
import static jodd.csselly.selector.Match.INCLUDES;

/**
 * CSS selector.
 */
public class CssSelector implements NodeFilter {

	private static final String ID = "id";
	private static final String CLASS = "class";

	protected CssSelector prevCssSelector;
	protected CssSelector nextCssSelector;

	protected final String element;

	protected Combinator combinator = Combinator.DESCENDANT;
	protected List<Selector> selectors;

	public CssSelector() {
		this(null);
	}

	public CssSelector(String element) {
		if (element == null) {
			element = StringPool.STAR;
		}
		this.element = unescape(element);
		this.selectors = new ArrayList<>();
	}

	/**
	 * Returns previous CssSelector.
	 */
	public CssSelector getPrevCssSelector() {
		return prevCssSelector;
	}

	/**
	 * Returns next CssSelector.
	 */
	public CssSelector getNextCssSelector() {
		return nextCssSelector;
	}

	// ---------------------------------------------------------------- init

	void setPrevCssSelector(final CssSelector prevCssSelector) {
		this.prevCssSelector = prevCssSelector;
		prevCssSelector.nextCssSelector = this;
	}

	// ---------------------------------------------------------------- selector

	/**
	 * Returns selector element name. Returns * for
	 * universal selectors.
	 */
	public String getElement() {
		return element;
	}

	/**
	 * Returns selector for given index.
	 */
	public Selector getSelector(final int index) {
		return selectors.get(index);
	}

	/**
	 * Returns number of selectors.
	 */
	public int selectorsCount() {
		return selectors.size();
	}

	// ---------------------------------------------------------------- combinator

	/**
	 * Sets combinator.
	 */
	public void setCombinator(final Combinator combinator) {
		this.combinator = combinator;
	}

	/**
	 * Returns combinator, may be null.
	 */
	public Combinator getCombinator() {
		return combinator;
	}

	// ---------------------------------------------------------------- attributes

	public void addIdSelector(String id) {
		id = unescape(id);
		selectors.add(new AttributeSelector(ID, EQUALS, id));
	}

	public void addClassSelector(String clazz) {
		clazz = unescape(clazz);
		selectors.add(new AttributeSelector(CLASS, INCLUDES, clazz));
	}

	public void addAttributeSelector(String attribute) {
		attribute = unescape(attribute);
		selectors.add(new AttributeSelector(attribute));
	}

	// ---------------------------------------------------------------- pseudo class

	public void addPseudoClassSelector(final String pseudoClass) {
		selectors.add(new PseudoClassSelector(pseudoClass));
	}

	public void addPseudoFunctionSelector(final String pseudoFunction, final String expression) {
		selectors.add(new PseudoFunctionSelector(pseudoFunction, expression));
	}

	// ---------------------------------------------------------------- string

	/**
	 * Generates CSS selector for the output.
	 */
	@Override
	public String toString() {
		StringBuilder out = new StringBuilder();
		out.append(element);

		for (Selector selector : selectors) {
			switch (selector.getType()) {
				case ATTRIBUTE:
					AttributeSelector attrSelector = (AttributeSelector) selector;
					String attrName = attrSelector.getName();
					if (attrName.equals(ID)) {
						out.append('#').append(attrSelector.getValue());
					} else if (attrName.equals(CLASS)) {
						out.append('.').append(attrSelector.getValue());
					} else {
						out.append('[').append(attrSelector.getName());
						String value = attrSelector.getValue();
						if (value != null) {
							out.append(attrSelector.getMatch().getSign());
							char quote = attrSelector.getQuoteChar();
							if (quote != 0) {
								out.append(quote);
							}
							out.append(value);
							if (quote != 0) {
								out.append(quote);
							}
						}
						out.append(']');
					}
					break;
				case PSEUDO_CLASS:
					PseudoClassSelector psc = (PseudoClassSelector) selector;
					out.append(':').append(psc.getPseudoClass().getPseudoClassName());
					break;
				case PSEUDO_FUNCTION:
					PseudoFunctionSelector pfns = (PseudoFunctionSelector) selector;
					out.append(':').append(pfns.getPseudoFunction().getPseudoFunctionName()).append('(');
					out.append(pfns.getExpression()).append(')');
					break;
			}
		}

		if (nextCssSelector != null) {
			if (combinator != Combinator.DESCENDANT) {
				out.append(' ');
			}
			out.append(combinator.getSign());
			if (combinator != Combinator.DESCENDANT) {
				out.append(' ');
			}
		}
		return out.toString();
	}

	// ---------------------------------------------------------------- match

	/**
	 * Accepts single node.
	 */
	@Override
	public boolean accept(final Node node) {
		// match element name with node name
		if (!matchElement(node)) {
			return false;
		}

		// match attributes
		int totalSelectors = selectorsCount();
		for (int i = 0; i < totalSelectors; i++) {
			Selector selector = getSelector(i);

			// just attr name existence
			switch (selector.getType()) {
				case ATTRIBUTE:
					if (!((AttributeSelector) selector).accept(node)) {
						return false;
					}
					break;
				case PSEUDO_CLASS:
					if (!((PseudoClassSelector) selector).accept(node)) {
						return false;
					}
					break;
				case PSEUDO_FUNCTION:
					if (!((PseudoFunctionSelector) selector).accept(node)) {
						return false;
					}
					break;
			}
		}
		return true;
	}

	/**
	 * Matches element to css selector. All non-element types are ignored.
	 */
	protected boolean matchElement(final Node node) {
		if (node.getNodeType() != Node.NodeType.ELEMENT) {
			return false;
		}
		String element = getElement();
		String nodeName = node.getNodeName();
		return element.equals(StringPool.STAR) || element.equals(nodeName);
	}

	// ---------------------------------------------------------------- post process

	/**
	 * Accepts node within current results.
	 */
	public boolean accept(final List<Node> currentResults, final Node node, final int index) {
		// match attributes
		int totalSelectors = selectorsCount();
		for (int i = 0; i < totalSelectors; i++) {
			Selector selector = getSelector(i);

			// just attr name existence
			switch (selector.getType()) {
				case PSEUDO_FUNCTION:
					if (!((PseudoFunctionSelector) selector).accept(currentResults, node, index)) {
						return false;
					}
					break;
				case PSEUDO_CLASS:
					if (!((PseudoClassSelector) selector).accept(currentResults, node, index)) {
						return false;
					}
					break;
				default:
			}
		}
		return true;
	}

	// ---------------------------------------------------------------- util

	/**
	 * Unescapes CSS string by removing all backslash characters from it.
	 */
	protected String unescape(final String value) {
		if (value.indexOf('\\') == -1) {
			return value;
		}

		return StringUtil.remove(value, '\\');
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.system;

import java.util.ArrayList;

abstract class JavaInfo extends HostInfo {

	private final String JAVA_VERSION = SystemUtil.get("java.version");
	private final int JAVA_VERSION_NUMBER = detectJavaVersionNumber();
	private final String JAVA_VENDOR = SystemUtil.get("java.vendor");
	private final String JAVA_VENDOR_URL = SystemUtil.get("java.vendor.url");
	private final String JAVA_SPECIFICATION_VERSION = SystemUtil.get("java.specification.version");
	private final String JAVA_SPECIFICATION_NAME = SystemUtil.get("java.specification.name");
	private final String JAVA_SPECIFICATION_VENDOR = SystemUtil.get("java.specification.vendor");
	private final String[] JRE_PACKAGES = buildJrePackages(JAVA_VERSION_NUMBER);

	/**
	 * Returns Java version string, as specified in system property.
	 * Returned string contain major version, minor version and revision.
	 */
	public String getJavaVersion() {
		return JAVA_VERSION;
	}

	/**
	 * Returns unified Java version as an integer.
	 */
	public int getJavaVersionNumber() {
		return JAVA_VERSION_NUMBER;
	}

	/**
	 * Returns Java vendor.
	 */
	public String getJavaVendor() {
		return JAVA_VENDOR;
	}

	/**
	 * Returns Java vendor URL.
	 */
	public String getJavaVendorURL() {
		return JAVA_VENDOR_URL;
	}

	/**
	 * Retrieves the version of the currently running JVM.
	 */
	public String getJavaSpecificationVersion() {
		return JAVA_SPECIFICATION_VERSION;
	}

	public final String getJavaSpecificationName() {
		return JAVA_SPECIFICATION_NAME;
	}

	public final String getJavaSpecificationVendor() {
		return JAVA_SPECIFICATION_VENDOR;
	}

	// ---------------------------------------------------------------- packages

	/**
	 * Returns list of packages, build into runtime jars.
	 */
	public String[] getJrePackages() {
		return JRE_PACKAGES;
	}

	/**
	 * Builds a set of java core packages.
	 */
	private String[] buildJrePackages(final int javaVersionNumber) {
		final ArrayList<String> packages = new ArrayList<>();

		switch (javaVersionNumber) {
			case 9:
			case 8:
			case 7:
			case 6:
			case 5:
				// in Java1.5, the apache stuff moved
				packages.add("com.sun.org.apache");
				// fall through...
			case 4:
				if (javaVersionNumber == 4) {
					packages.add("org.apache.crimson");
					packages.add("org.apache.xalan");
					packages.add("org.apache.xml");
					packages.add("org.apache.xpath");
				}
				packages.add("org.ietf.jgss");
				packages.add("org.w3c.dom");
				packages.add("org.xml.sax");
				// fall through...
			case 3:
				packages.add("org.omg");
				packages.add("com.sun.corba");
				packages.add("com.sun.jndi");
				packages.add("com.sun.media");
				packages.add("com.sun.naming");
				packages.add("com.sun.org.omg");
				packages.add("com.sun.rmi");
				packages.add("sunw.io");
				packages.add("sunw.util");
				// fall through...
			case 2:
				packages.add("com.sun.java");
				packages.add("com.sun.image");
				// fall through...
			case 1:
			default:
				// core stuff
				packages.add("sun");
				packages.add("java");
				packages.add("javax");
				break;
		}

		return packages.toArray(new String[0]);
	}

	// ---------------------------------------------------------------- java checks

	private int detectJavaVersionNumber() {
		String javaVersion = JAVA_VERSION;

		final int lastDashNdx = javaVersion.lastIndexOf('-');
		if (lastDashNdx != -1) {
			javaVersion = javaVersion.substring(0, lastDashNdx);
		}

		if (javaVersion.startsWith("1.")) {
			// up to java 8
			final int index = javaVersion.indexOf('.', 2);
			return Integer.parseInt(javaVersion.substring(2, index));
		} else {
			final int index = javaVersion.indexOf('.');
			return Integer.parseInt(index == -1 ? javaVersion : javaVersion.substring(0, index));
		}
	}

	/**
	 * Checks if the currently running JVM is at least compliant
	 * with provided JDK version.
	 */
	public boolean isAtLeastJavaVersion(final int version) {
		return JAVA_VERSION_NUMBER >= version;
	}

	/**
	 * Checks if the currently running JVM is equal to provided version.
	 */
	public boolean isJavaVersion(final int version) {
		return JAVA_VERSION_NUMBER == version;
	}

	@Override
	public String toString() {
		return
			super.toString() +
			"\nJava version:       " + getJavaVersion() +
			"\nJava vendor:        " + getJavaVendor() +
			"\nJava vendor URL:    " + getJavaVendorURL() +
			"\nJava spec. name:    " + getJavaSpecificationName() +
			"\nJava spec. version: " + getJavaSpecificationVersion() +
			"\nJava spec. vendor:  " + getJavaSpecificationVendor();
	}
}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.lagarto.dom;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

/**
 * DOM node.
 */
@SuppressWarnings({"ForLoopReplaceableByForEach", "ClassReferencesSubclass"})
public abstract class Node implements Cloneable {

	/**
	 * Node types.
	 */
	public enum NodeType {
		DOCUMENT, ELEMENT, TEXT, COMMENT, CDATA, DOCUMENT_TYPE, XML_DECLARATION
	}

	// node values

	protected final String nodeName;
	protected final String nodeRawName;
	protected final NodeType nodeType;
	protected Document ownerDocument;	// root document node
	protected String nodeValue;

	// attributes

	protected List<Attribute> attributes;

	// parent

	protected Node parentNode;

	// children

	protected List<Node> childNodes;
	protected int childElementNodesCount;
	protected Element[] childElementNodes;

	// siblings

	protected int siblingIndex;
	protected int siblingElementIndex = -1;
	protected int siblingNameIndex = -1;

	/**
	 * Creates new node.
	 */
	protected Node(final Document document, final NodeType nodeType, final String nodeName) {
		this.ownerDocument = document;
		this.nodeRawName = nodeName;
		if (nodeName != null) {
			this.nodeName = ownerDocument.config.isCaseSensitive() ? nodeName : nodeName.toLowerCase();
		} else {
			this.nodeName = null;
		}
		this.nodeType = nodeType;
	}

	// ---------------------------------------------------------------- clone

	/**
	 * Copies all non-final values to the empty cloned object.
	 * Cache-related values are not copied.
	 */
	protected <T extends Node> T cloneTo(final T dest) {
//		dest.nodeValue = nodeValue;		// already  in clone implementations!
		dest.parentNode = parentNode;

		if (attributes != null) {
			dest.attributes = new ArrayList<>(attributes.size());
			for (int i = 0, attributesSize = attributes.size(); i < attributesSize; i++) {
				Attribute attr = attributes.get(i);
				dest.attributes.add(attr.clone());
			}
		}

		if (childNodes != null) {
			dest.childNodes = new ArrayList<>(childNodes.size());
			for (int i = 0, childNodesSize = childNodes.size(); i < childNodesSize; i++) {
				Node child = childNodes.get(i);
				Node childClone = child.clone();

				childClone.parentNode = dest;    // fix parent!
				dest.childNodes.add(childClone);
			}
		}

		return dest;
	}

	@Override
	public abstract Node clone();

	// ---------------------------------------------------------------- basic

	/**
	 * Returns {@link NodeType node type}.
	 */
	public NodeType getNodeType() {
		return nodeType;
	}

	/**
	 * Returns nodes name or null if name is not available.
	 */
	public String getNodeName() {
		return nodeName;
	}

	/**
	 * Returns nodes raw name - exactly as it was given in the input.
	 */
	public String getNodeRawName() {
		return nodeRawName;
	}

	/**
	 * Returns node value or null if value is not available.
	 */
	public String getNodeValue() {
		return nodeValue;
	}

	/**
	 * Sets node value.
	 */
	public void setNodeValue(final String value) {
		this.nodeValue = value;
	}

	/**
	 * Returns owner document, root node for this DOM tree.
	 */
	public Document getOwnerDocument() {
		return ownerDocument;
	}

	// ---------------------------------------------------------------- tree

	/**
	 * Removes this node from DOM tree.
	 */
	public void detachFromParent() {
		if (parentNode == null) {
			return;
		}
		if (parentNode.childNodes != null) {
			parentNode.childNodes.remove(siblingIndex);
			parentNode.reindexChildren();
		}
		parentNode = null;
	}

	/**
	 * Appends child node. Don't use this node in the loop,
	 * since it might be slow due to {@link #reindexChildren()}.
	 */
	public void addChild(final Node node) {
		node.detachFromParent();
		node.parentNode = this;
		initChildNodes(node);
		childNodes.add(node);
		reindexChildrenOnAdd(1);
	}

	/**
	 * Appends several child nodes at once.
	 * Reindex is done only once, after all children are added.
	 */
	public void addChild(final Node... nodes) {
		if (nodes.length == 0) {
			return;	// nothing to add
		}
		for (Node node : nodes) {
			node.detachFromParent();
			node.parentNode = this;
			initChildNodes(node);
			childNodes.add(node);
		}
		reindexChildrenOnAdd(nodes.length);
	}

	/**
	 * Inserts node at given index.
	 */
	public void insertChild(final Node node, final int index) {
		node.detachFromParent();
		node.parentNode = this;
		try {
			initChildNodes(node);
			childNodes.add(index, node);
		} catch (IndexOutOfBoundsException ignore) {
			throw new LagartoDOMException("Invalid node index: " + index);
		}
		reindexChildren();
	}

	/**
	 * Inserts several nodes at ones. Reindex is done onl once,
	 * after all children are added.
	 */
	public void insertChild(final Node[] nodes, int index) {
		for (Node node : nodes) {
			node.detachFromParent();
			node.parentNode = this;
			try {
				initChildNodes(node);
				childNodes.add(index, node);
				index++;
			} catch (IndexOutOfBoundsException ignore) {
				throw new LagartoDOMException("Invalid node index: " + index);
			}
		}
		reindexChildren();
	}

	/**
	 * Inserts node before provided node.
	 */
	public void insertBefore(final Node newChild, final Node refChild) {
		int siblingIndex = refChild.getSiblingIndex();
		refChild.parentNode.insertChild(newChild, siblingIndex);
	}

	/**
	 * Inserts several child nodes before provided node.
	 */
	public void insertBefore(final Node[] newChilds, final Node refChild) {
		if (newChilds.length == 0) {
			return;
		}
		int siblingIndex = refChild.getSiblingIndex();
		refChild.parentNode.insertChild(newChilds, siblingIndex);
	}

	/**
	 * Inserts node after provided node.
	 */
	public void insertAfter(final Node newChild, final Node refChild) {
		int siblingIndex = refChild.getSiblingIndex() + 1;
		if (siblingIndex == refChild.parentNode.getChildNodesCount()) {
			refChild.parentNode.addChild(newChild);
		} else {
			refChild.parentNode.insertChild(newChild, siblingIndex);
		}
	}

	/**
	 * Inserts several child nodes after referent node.
	 */
	public void insertAfter(final Node[] newChilds, final Node refChild) {
		if (newChilds.length == 0) {
			return;
		}

		int siblingIndex = refChild.getSiblingIndex() + 1;
		if (siblingIndex == refChild.parentNode.getChildNodesCount()) {
			refChild.parentNode.addChild(newChilds);
		} else {
			refChild.parentNode.insertChild(newChilds, siblingIndex);
		}
	}

	/**
	 * Removes child node at given index.
	 * Returns removed node or null if index is invalid.
	 */
	public Node removeChild(final int index) {
		if (childNodes == null) {
			return null;
		}
		Node node;
		try {
			node = childNodes.get(index);
		} catch (IndexOutOfBoundsException ignore) {
			return null;
		}
		node.detachFromParent();
		return node;
	}

	/**
	 * Removes child node. It works only with direct children, i.e.
	 * if provided child node is not a child nothing happens.
	 */
	public void removeChild(final Node childNode) {
		if (childNode.getParentNode() != this) {
			return;
		}
		childNode.detachFromParent();
	}

	/**
	 * Removes all child nodes. Each child node will be detached from this parent.
	 */
	public void removeAllChilds() {
		List<Node> removedNodes = childNodes;
		childNodes = null;
		childElementNodes = null;
		childElementNodesCount = 0;

		if (removedNodes != null) {
			for (int i = 0, removedNodesSize = removedNodes.size(); i < removedNodesSize; i++) {
				Node removedNode = removedNodes.get(i);
				removedNode.detachFromParent();
			}
		}
	}

	/**
	 * Returns parent node or null if no parent exist.
	 */
	public Node getParentNode() {
		return parentNode;
	}

	// ---------------------------------------------------------------- attributes

	/**
	 * Returns true if node has attributes.
	 */
	public boolean hasAttributes() {
		if (attributes == null) {
			return false;
		}
		return !attributes.isEmpty();
	}

	/**
	 * Returns total number of attributes.
	 */
	public int getAttributesCount() {
		if (attributes == null) {
			return 0;
		}
		return attributes.size();
	}

	/**
	 * Returns attribute at given index or null if index not found.
	 */
	public Attribute getAttribute(final int index) {
		if (attributes == null) {
			return null;
		}
		if ((index < 0) || (index >= attributes.size())) {
			return null;
		}
		return attributes.get(index);
	}

	/**
	 * Returns true if node contains an attribute.
	 */
	public boolean hasAttribute(String name) {
		if (attributes == null) {
			return false;
		}
		if (!ownerDocument.config.isCaseSensitive()) {
			name = name.toLowerCase();
		}
		for (int i = 0, attributesSize = attributes.size(); i < attributesSize; i++) {
			Attribute attr = attributes.get(i);
			if (attr.getName().equals(name)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns attribute value. Returns null when
	 * attribute doesn't exist or when attribute exist but doesn't
	 * specify a value.
	 */
	public String getAttribute(final String name) {
		Attribute attribute = getAttributeInstance(name);
		if (attribute == null) {
			return null;
		}
		return attribute.getValue();
	}

	protected Attribute getAttributeInstance(String name) {
		if (attributes == null) {
			return null;
		}

		if (!ownerDocument.config.isCaseSensitive()) {
			name = name.toLowerCase();
		}

		for (int i = 0, attributesSize = attributes.size(); i < attributesSize; i++) {
			Attribute attr = attributes.get(i);
			if (attr.getName().equals(name)) {
				return attr;
			}
		}
		return null;
	}

	protected int indexOfAttributeInstance(String name) {
		if (attributes == null) {
			return -1;
		}

		if (!ownerDocument.config.isCaseSensitive()) {
			name = name.toLowerCase();
		}

		for (int i = 0, attributesSize = attributes.size(); i < attributesSize; i++) {
			Attribute attr = attributes.get(i);
			if (attr.getName().equals(name)) {
				return i;
			}
		}
		return -1;
	}

	public boolean removeAttribute(final String name) {
		int index = indexOfAttributeInstance(name);
		if (index == -1) {
			return false;
		}
		attributes.remove(index);
		return true;
	}

	/**
	 * Sets attribute value. Value may be null.
	 */
	public void setAttribute(String name, final String value) {
		initAttributes();

		String rawAttributeName = name;
		if (!ownerDocument.config.isCaseSensitive()) {
			name = name.toLowerCase();
		}

		// search if attribute with the same name exist
		for (int i = 0, attributesSize = attributes.size(); i < attributesSize; i++) {
			Attribute attr = attributes.get(i);
			if (attr.getName().equals(name)) {
				attr.setValue(value);
				return;
			}
		}
		attributes.add(new Attribute(rawAttributeName, name, value));
	}

	/**
	 * Sets attribute that doesn't need a value.
	 */
	public void setAttribute(final String name) {
		setAttribute(name, null);
	}

	/**
	 * Returns true if attribute containing some word.
	 */
	public boolean isAttributeContaining(final String name, final String word) {
		Attribute attr = getAttributeInstance(name);
		if (attr == null) {
			return false;
		}
		return attr.isContaining(word);
	}

	// ---------------------------------------------------------------- children count

	/**
	 * Returns true if node has child nodes.
	 */
	public boolean hasChildNodes() {
		if (childNodes == null) {
			return false;
		}
		return !childNodes.isEmpty();
	}

	/**
	 * Returns number of all child nodes.
	 */
	public int getChildNodesCount() {
		if (childNodes == null) {
			return 0;
		}
		return childNodes.size();
	}

	/**
	 * Returns number of child elements.
	 */
	public int getChildElementsCount() {
		return childElementNodesCount;
	}

	/**
	 * Returns number of child elements with given name.
	 */
	public int getChildElementsCount(final String elementName) {
		Node lastChild = getLastChildElement(elementName);
		return lastChild.siblingNameIndex + 1;
	}

	// ---------------------------------------------------------------- children

	/**
	 * Returns an array of all children nodes. Returns an empty array
	 * if there are no children.
	 */
	public Node[] getChildNodes() {
		if (childNodes == null) {
			return new Node[0];
		}
		return childNodes.toArray(new Node[0]);
	}

	/**
	 * Finds the first child node with given node name.
	 */
	public Node findChildNodeWithName(final String name) {
		if (childNodes == null) {
			return null;
		}
		for (final Node childNode : childNodes) {
			if (childNode.getNodeName().equals(name)) {
				return childNode;
			}
		}
		return null;
	}

	/**
	 * Filters child nodes.
	 */
	public Node[] filterChildNodes(final Predicate<Node> nodePredicate) {
		if (childNodes == null) {
			return new Node[0];
		}
		return childNodes.stream()
			.filter(nodePredicate)
			.toArray(Node[]::new);
	}

	/**
	 * Returns an array of all children elements.
	 */
	public Element[] getChildElements() {
		initChildElementNodes();
		return childElementNodes.clone();
	}

	/**
	 * Returns a child node at given index or null
	 * if child doesn't exist for that index.
	 */
	public Node getChild(final int index) {
		if (childNodes == null) {
			return null;
		}
		if ((index < 0) || (index >= childNodes.size())) {
			return null;
		}
		return childNodes.get(index);
	}

	/**
	 * Returns a child node with given hierarchy.
	 * Just a shortcut for successive calls of {@link #getChild(int)}.
	 */
	public Node getChild(final int... indexes) {
		Node node = this;
		for (int index : indexes) {
			node = node.getChild(index);
		}
		return node;
	}

	/**
	 * Returns a child element node at given index.
	 * If index is out of bounds, null is returned.
	 */
	public Element getChildElement(final int index) {
		initChildElementNodes();
		if ((index < 0) || (index >= childElementNodes.length)) {
			return null;
		}
		return childElementNodes[index];
	}

	// ---------------------------------------------------------------- first child

	/**
	 * Returns first child or null if no children exist.
	 */
	public Node getFirstChild() {
		if (childNodes == null) {
			return null;
		}
		if (childNodes.isEmpty()) {
			return null;
		}
		return childNodes.get(0);
	}

	/**
	 * Returns first child element node or
	 * null if no element children exist.
	 */
	public Element getFirstChildElement() {
		initChildElementNodes();
		if (childElementNodes.length == 0) {
			return null;
		}
		return childElementNodes[0];
	}

	/**
	 * Returns first child element with given name or
	 * null if no such children exist.
	 */
	public Element getFirstChildElement(final String elementName) {
		if (childNodes == null) {
			return null;
		}
		for (int i = 0, childNodesSize = childNodes.size(); i < childNodesSize; i++) {
			Node child = childNodes.get(i);
			if (child.getNodeType() == NodeType.ELEMENT && elementName.equals(child.getNodeName())) {
				child.initSiblingNames();
				return (Element) child;
			}
		}
		return null;
	}

	// ---------------------------------------------------------------- last child

	/**
	 * Returns last child or null if no children exist.
	 */
	public Node getLastChild() {
		if (childNodes == null) {
			return null;
		}
		if (childNodes.isEmpty()) {
			return null;
		}
		return childNodes.get(getChildNodesCount() - 1);
	}

	/**
	 * Returns last child element node or
	 * null if no such child node exist.
	 */
	public Element getLastChildElement() {
		initChildElementNodes();
		if (childElementNodes.length == 0) {
			return null;
		}
		return childElementNodes[childElementNodes.length - 1];
	}

	/**
	 * Returns last child element with given name or
	 * null if no such child node exist.
	 */
	public Element getLastChildElement(final String elementName) {
		if (childNodes == null) {
			return null;
		}
		int from = childNodes.size() - 1;
		for (int i = from; i >= 0; i--) {
			Node child = childNodes.get(i);
			if (child.getNodeType() == NodeType.ELEMENT && elementName.equals(child.getNodeName())) {
				child.initSiblingNames();
				return (Element) child;
			}
		}
		return null;
	}

	// ---------------------------------------------------------------- internal

	/**
	 * Checks the health of child nodes. Useful during complex tree manipulation,
	 * to check if everything is OK. Not optimized for speed, should be used just
	 * for testing purposes.
	 */
	public boolean check() {

		if (childNodes == null) {
			return true;
		}

		// children
		int siblingElementIndex = 0;
		for (int i = 0, childNodesSize = childNodes.size(); i < childNodesSize; i++) {
			Node childNode = childNodes.get(i);

			if (childNode.siblingIndex != i) {
				return false;
			}

			if (childNode.getNodeType() == NodeType.ELEMENT) {
				if (childNode.siblingElementIndex != siblingElementIndex) {
					return false;
				}
				siblingElementIndex++;
			}
		}

		if (childElementNodesCount != siblingElementIndex) {
			return false;
		}

		// child element nodes
		if (childElementNodes != null) {
			if (childElementNodes.length != childElementNodesCount) {
				return false;
			}

			int childCount = getChildNodesCount();
			for (int i = 0; i < childCount; i++) {
				Node child = getChild(i);
				if (child.siblingElementIndex >= 0) {
					if (childElementNodes[child.siblingElementIndex] != child) {
						return false;
					}
				}
			}
		}

		// sibling names
		if (siblingNameIndex != -1) {
			List<Node> siblings = parentNode.childNodes;
			int index = 0;
			for (int i = 0, siblingsSize = siblings.size(); i < siblingsSize; i++) {
				Node sibling = siblings.get(i);
				if (sibling.siblingNameIndex == -1
						&& nodeType == NodeType.ELEMENT
						&& nodeName.equals(sibling.getNodeName())) {
					if (sibling.siblingNameIndex != index++) {
						return false;
					}
				}
			}
		}

		// process children
		for (Node childNode : childNodes) {
			if (!childNode.check()) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Reindex children nodes. Must be called on every children addition/removal.
	 * Iterates {@link #childNodes} list and:
	 *

	 *
calculates three different sibling indexes,

	 *
calculates total child element node count,

	 *
resets child element nodes array (will be init lazy later by @{#initChildElementNodes}.

	 *

	 */
	protected void reindexChildren() {
		int siblingElementIndex = 0;
		for (int i = 0, childNodesSize = childNodes.size(); i < childNodesSize; i++) {
			Node childNode = childNodes.get(i);

			childNode.siblingIndex = i;
			childNode.siblingNameIndex = -1;	// reset sibling name info
			if (childNode.getNodeType() == NodeType.ELEMENT) {
				childNode.siblingElementIndex = siblingElementIndex;
				siblingElementIndex++;
			}
		}

		childElementNodesCount = siblingElementIndex;
		childElementNodes = null;	// reset child element nodes
	}

	/**
	 * Optimized variant of {@link #reindexChildren()} for addition.
	 * Only added children are optimized.
	 */
	protected void reindexChildrenOnAdd(final int addedCount) {
		int childNodesSize = childNodes.size();
		int previousSize = childNodes.size() - addedCount;

		int siblingElementIndex = childElementNodesCount;
		for (int i = previousSize; i < childNodesSize; i++) {
			Node childNode = childNodes.get(i);

			childNode.siblingIndex = i;
			childNode.siblingNameIndex = -1;	// reset sibling name info
			if (childNode.getNodeType() == NodeType.ELEMENT) {
				childNode.siblingElementIndex = siblingElementIndex;
				siblingElementIndex++;
			}
		}

		childElementNodesCount = siblingElementIndex;
		childElementNodes = null;	// reset child element nodes
	}

	/**
	 * Initializes list of child elements.
	 */
	protected void initChildElementNodes() {
		if (childElementNodes == null) {
			childElementNodes = new Element[childElementNodesCount];

			int childCount = getChildNodesCount();
			for (int i = 0; i < childCount; i++) {
				Node child = getChild(i);
				if (child.siblingElementIndex >= 0) {
					childElementNodes[child.siblingElementIndex] = (Element) child;
				}
			}
		}
	}

	/**
	 * Initializes siblings elements of the same name.
	 */
	protected void initSiblingNames() {
		if (siblingNameIndex == -1) {
			List<Node> siblings = parentNode.childNodes;
			int index = 0;
			for (int i = 0, siblingsSize = siblings.size(); i < siblingsSize; i++) {
				Node sibling = siblings.get(i);
				if (sibling.siblingNameIndex == -1
						&& nodeType == NodeType.ELEMENT
						&& nodeName.equals(sibling.getNodeName())) {
					sibling.siblingNameIndex = index++;
				}
			}
		}
	}

	/**
	 * Initializes attributes when needed.
	 */
	protected void initAttributes() {
		if (attributes == null) {
			attributes = new ArrayList<>(5);
		}
	}

	/**
	 * Initializes child nodes list when needed.
	 * Also fix owner document for new node, if needed.
	 */
	protected void initChildNodes(final Node newNode) {
		if (childNodes == null) {
			childNodes = new ArrayList<>();
		}
		if (ownerDocument != null) {
			if (newNode.ownerDocument != ownerDocument) {
				changeOwnerDocument(newNode, ownerDocument);
			}
		}
	}

	/**
	 * Changes owner document for given node and all its children.
	 */
	protected void changeOwnerDocument(final Node node, final Document ownerDocument) {
		node.ownerDocument = ownerDocument;

		int childCount = node.getChildNodesCount();
		for (int i = 0; i < childCount; i++) {
			Node child = node.getChild(i);
			changeOwnerDocument(child, ownerDocument);
		}
	}

	// ---------------------------------------------------------------- siblings index

	/**
	 * Get the list index of this node in its node sibling list.
	 * For example, if this is the first node sibling, returns 0.
	 * Index address all nodes, i.e. of all node types.
	 */
	public int getSiblingIndex() {
		return siblingIndex;
	}

	public int getSiblingElementIndex() {
		return siblingElementIndex;
	}

	public int getSiblingNameIndex() {
		initSiblingNames();
		return siblingNameIndex;
	}

	// ---------------------------------------------------------------- next

	/**
	 * Returns this node's next sibling of any type or
	 * null if this is the last sibling.
	 */
	public Node getNextSibling() {
		List<Node> siblings = parentNode.childNodes;
		int index = siblingIndex + 1;
		if (index >= siblings.size()) {
			return null;
		}
		return siblings.get(index);
	}

	/**
	 * Returns this node's next element.
	 */
	public Node getNextSiblingElement() {
		parentNode.initChildElementNodes();
		if (siblingElementIndex == -1) {
			int max = parentNode.getChildNodesCount();
			for (int i = siblingIndex; i < max; i++) {
				Node sibling = parentNode.childNodes.get(i);
				if (sibling.getNodeType() == NodeType.ELEMENT) {
					return sibling;
				}
			}
			return null;
		}
		int index = siblingElementIndex + 1;
		if (index >= parentNode.childElementNodesCount) {
			return null;
		}
		return parentNode.childElementNodes[index];
	}

	/**
	 * Returns this node's next element with the same name.
	 */
	public Node getNextSiblingName() {
		if (nodeName == null) {
			return null;
		}
		initSiblingNames();
		int index = siblingNameIndex + 1;
		int max = parentNode.getChildNodesCount();
		for (int i = siblingIndex + 1; i < max; i++) {
			Node sibling = parentNode.childNodes.get(i);
			if ((index == sibling.siblingNameIndex) && nodeName.equals(sibling.getNodeName())) {
				return sibling;
			}
		}
		return null;
	}

	// ---------------------------------------------------------------- prev

	/**
	 * Returns this node's previous sibling of any type
	 * or null if this is the first sibling.
	 */
	public Node getPreviousSibling() {
		List<Node> siblings = parentNode.childNodes;
		int index = siblingIndex - 1;
		if (index < 0) {
			return null;
		}
		return siblings.get(index);

	}

	/**
	 * Returns this node's previous sibling of element type
	 * or null if this is the first sibling.
	 */
	public Node getPreviousSiblingElement() {
		parentNode.initChildElementNodes();
		if (siblingElementIndex == -1) {
			for (int i = siblingIndex - 1; i >= 0; i--) {
				Node sibling = parentNode.childNodes.get(i);
				if (sibling.getNodeType() == NodeType.ELEMENT) {
					return sibling;
				}
			}
			return null;
		}
		int index = siblingElementIndex - 1;
		if (index < 0) {
			return null;
		}
		return parentNode.childElementNodes[index];
	}

	/**
	 * Returns this node's previous sibling element with the same name.
	 */
	public Node getPreviousSiblingName() {
		if (nodeName == null) {
			return null;
		}
		initSiblingNames();
		int index = siblingNameIndex -1;
		for (int i = siblingIndex; i >= 0; i--) {
			Node sibling = parentNode.childNodes.get(i);
			if ((index == sibling.siblingNameIndex) && nodeName.equals(sibling.getNodeName())) {
				return sibling;
			}
		}
		return null;
	}

	// ---------------------------------------------------------------- html

	/**
	 * Returns the text content of this node and its descendants.
	 * @see #appendTextContent(Appendable)
	 */
	public String getTextContent() {
		StringBuilder sb = new StringBuilder(getChildNodesCount() + 1);
		appendTextContent(sb);
		return sb.toString();
	}

	/**
	 * Appends the text content to an Appendable
	 * (StringBuilder, CharBuffer...).
	 * This way we can reuse the Appendable instance
	 * during the creation of text content and have better performances.
	 */
	public void appendTextContent(final Appendable appendable) {
		if (nodeValue != null) {
			if ((nodeType == NodeType.TEXT) || (nodeType == NodeType.CDATA)) {
				try {
					appendable.append(nodeValue);
				} catch (IOException ioex) {
					throw new LagartoDOMException(ioex);
				}
			}
		}
		if (childNodes != null) {
			for (int i = 0, childNodesSize = childNodes.size(); i < childNodesSize; i++) {
				Node childNode = childNodes.get(i);
				childNode.appendTextContent(appendable);
			}
		}
	}

	// ---------------------------------------------------------------- visit

	/**
	 * Generates HTML.
	 */
	public String getHtml() {
		LagartoDomBuilderConfig lagartoDomBuilderConfig;
		if (ownerDocument == null) {
			lagartoDomBuilderConfig = ((Document) this).getConfig();
		} else {
			lagartoDomBuilderConfig = ownerDocument.getConfig();
		}

		LagartoHtmlRenderer lagartoHtmlRenderer =
				lagartoDomBuilderConfig.getLagartoHtmlRenderer();

		return lagartoHtmlRenderer.toHtml(this, new StringBuilder());
	}

	/**
	 * Generates inner HTML.
	 */
	public String getInnerHtml() {
		LagartoDomBuilderConfig lagartoDomBuilderConfig;
		if (ownerDocument == null) {
			lagartoDomBuilderConfig = ((Document) this).getConfig();
		} else {
			lagartoDomBuilderConfig = ownerDocument.getConfig();
		}

		LagartoHtmlRenderer lagartoHtmlRenderer =
				lagartoDomBuilderConfig.getLagartoHtmlRenderer();

		return lagartoHtmlRenderer.toInnerHtml(this, new StringBuilder());
	}

	/**
	 * Visits the DOM tree.
	 */
	public void visit(final NodeVisitor nodeVisitor) {
		visitNode(nodeVisitor);
	}

	/**
	 * Visits children nodes.
	 */
	protected void visitChildren(final NodeVisitor nodeVisitor) {
		if (childNodes != null) {
			for (int i = 0, childNodesSize = childNodes.size(); i < childNodesSize; i++) {
				Node childNode = childNodes.get(i);
				childNode.visit(nodeVisitor);
			}
		}
	}

	/**
	 * Visits single node. Implementations just needs to call
	 * the correct visitor callback function.
	 */
	protected abstract void visitNode(NodeVisitor nodeVisitor);

	// ---------------------------------------------------------------- misc

	/**
	 * Returns CSS path to this node from document root.
	 */
	public String getCssPath() {
		StringBuilder path = new StringBuilder();

		Node node = this;
		while (node != null) {
			String nodeName = node.getNodeName();
			if (nodeName != null) {
				StringBuilder sb = new StringBuilder();
				sb.append(' ').append(nodeName);
				String id = node.getAttribute("id");
				if (id != null) {
					sb.append('#').append(id);
				}
				path.insert(0, sb);
			}
			node = node.getParentNode();
		}

		if (path.charAt(0) == ' ') {
			return path.substring(1);
		}
		return path.toString();
	}

}


// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.decora.parser;

import jodd.decora.DecoraException;
import jodd.lagarto.EmptyTagVisitor;
import jodd.lagarto.Tag;
import jodd.lagarto.TagType;

import java.util.ArrayList;

/**
 * Visitor that detect Decora tags in the decorator.
 * Therefore, it determines Decora tags position inside decorator file.
 * <p>
 * If decorator content is static, array of Decora tags can be cached
 * and {@link jodd.decora.parser.DecoraTag#duplicate() duplicated} to
 * skip parsing decorator again.
 */
public class DecoratorTagVisitor extends EmptyTagVisitor {

	protected ArrayList<DecoraTag> decoraTags = new ArrayList<>();

	/**
	 * Returns an array of founded Decora tags.
	 */
	public DecoraTag[] getDecoraTags() {
		return decoraTags.toArray(new DecoraTag[0]);
	}

	protected String decoraTagName;
	protected String decoraIdName;
	protected int decoraTagStart;
	protected int decoraTagEnd;
	protected int decoraTagDefaultValueStart;
	protected int decoraTagDefaultValueEnd;

	protected String closingTagName;
	protected int closingTagDeepLevel;

	@Override
	public void tag(final Tag tag) {
		String tagName = tag.getName().toString();

		if (tagName.startsWith("decora:") ) {
			onDecoraTag(tag);
			return;
		}
		if (tag.getType().isStartingTag()) {
			CharSequence id = tag.getId();

			if (id != null && id.toString().startsWith("decora-")) {
				onIdAttrStart(tag);
			}
		} else {
			// close tag
			if (tagName.equals(closingTagName) && closingTagDeepLevel == tag.getDeepLevel()) {
				onIdAttrEnd(tag);
			}
		}
	}

	// ---------------------------------------------------------------- handlers

	/**
	 * Handle Decora tags.
	 */
	protected void onDecoraTag(final Tag tag) {
		String tagName = tag.getName().toString();

		if (tag.getType() == TagType.SELF_CLOSING) {
			checkNestedDecoraTags();
			decoraTagName = tagName.substring(7);
			decoraTagStart = tag.getTagPosition();
			decoraTagEnd = tag.getTagPosition() + tag.getTagLength();
			defineDecoraTag();
			return;
		}

		if (tag.getType() == TagType.START) {
			checkNestedDecoraTags();
			decoraTagName = tagName.substring(7);
			decoraTagStart = tag.getTagPosition();
			decoraTagDefaultValueStart = tag.getTagPosition() + tag.getTagLength();
			return;
		}

		// closed tag type
		decoraTagEnd = tag.getTagPosition() + tag.getTagLength();
		decoraTagDefaultValueEnd = tag.getTagPosition();
		defineDecoraTag();
	}

	/**
	 * Handle open and empty ID attribute tags.
	 */
	protected void onIdAttrStart(final Tag tag) {
		String id = tag.getId().toString().substring(7);
		String tagName;
		String idName;

		int dashIndex = id.indexOf('-');
		if (dashIndex == -1) {
			tagName = id;
			idName = null;
		} else {
			tagName = id.substring(0, dashIndex);
			idName = id.substring(dashIndex + 1);
		}

		if (tag.getType() == TagType.SELF_CLOSING) {
			checkNestedDecoraTags();
			decoraTagName = tagName;
			decoraIdName = idName;
			decoraTagStart = tag.getTagPosition();
			decoraTagEnd = tag.getTagPosition() + tag.getTagLength();
			defineDecoraTag();
			return;
		}

		if (tag.getType() == TagType.START) {
			checkNestedDecoraTags();
			decoraTagName = tagName;
			decoraIdName = idName;
			decoraTagStart = tag.getTagPosition();
			decoraTagDefaultValueStart = tag.getTagPosition() + tag.getTagLength();

			closingTagName = tag.getName().toString();
			closingTagDeepLevel = tag.getDeepLevel();
		}
	}

	protected void onIdAttrEnd(final Tag tag) {
		decoraTagEnd = tag.getTagPosition() + tag.getTagLength();
		decoraTagDefaultValueEnd = tag.getTagPosition();
		defineDecoraTag();
	}

	// ---------------------------------------------------------------- define

	/**
	 * Defines Decora tag position inside decorator content.
	 * Resets current Decora tag tracking.
	 */
	protected void defineDecoraTag() {
		DecoraTag decoraTag =
				decoraTagDefaultValueStart == 0 ?
					new DecoraTag(decoraTagName, decoraIdName, decoraTagStart, decoraTagEnd) :
					new DecoraTag(
							decoraTagName, decoraIdName,
							decoraTagStart, decoraTagEnd,
							decoraTagDefaultValueStart, decoraTagDefaultValueEnd - decoraTagDefaultValueStart);

		decoraTags.add(decoraTag);
		decoraTagName = null;
		decoraIdName = null;
		closingTagName = null;
		decoraTagDefaultValueStart = 0;
	}

	// ---------------------------------------------------------------- tools

	/**
	 * Check if decora tag is currently defined and throws an exception
	 * on nested tags.
	 */
	protected void checkNestedDecoraTags() {
		if (decoraTagName != null) {
			throw new DecoraException("Nested Decora tags not allowed");
		}
	}
}