// ASM: a very small and fast Java bytecode manipulation framework
// Copyright (c) 2000-2011 INRIA, France Telecom
// 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.
// 3. Neither the name of the copyright holders nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// 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 OWNER 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.asm7;

/**
 * Information about an exception handler. Corresponds to an element of the exception_table array of
 * a Code attribute, as defined in the Java Virtual Machine Specification (JVMS). Handler instances
 * can be chained together, with their {@link #nextHandler} field, to describe a full JVMS
 * exception_table array.
 *
 * @see JVMS
 *     4.7.3
 * @author Eric Bruneton
 */
final class Handler {

  /**
   * The start_pc field of this JVMS exception_table entry. Corresponds to the beginning of the
   * exception handler's scope (inclusive).
   */
  final Label startPc;

  /**
   * The end_pc field of this JVMS exception_table entry. Corresponds to the end of the exception
   * handler's scope (exclusive).
   */
  final Label endPc;

  /**
   * The handler_pc field of this JVMS exception_table entry. Corresponding to the beginning of the
   * exception handler's code.
   */
  final Label handlerPc;

  /**
   * The catch_type field of this JVMS exception_table entry. This is the constant pool index of the
   * internal name of the type of exceptions handled by this handler, or 0 to catch any exceptions.
   */
  final int catchType;

  /**
   * The internal name of the type of exceptions handled by this handler, or {@literal null} to
   * catch any exceptions.
   */
  final String catchTypeDescriptor;

  /** The next exception handler. */
  Handler nextHandler;

  /**
   * Constructs a new Handler.
   *
   * @param startPc the start_pc field of this JVMS exception_table entry.
   * @param endPc the end_pc field of this JVMS exception_table entry.
   * @param handlerPc the handler_pc field of this JVMS exception_table entry.
   * @param catchType The catch_type field of this JVMS exception_table entry.
   * @param catchTypeDescriptor The internal name of the type of exceptions handled by this handler,
   *     or {@literal null} to catch any exceptions.
   */
  Handler(
      final Label startPc,
      final Label endPc,
      final Label handlerPc,
      final int catchType,
      final String catchTypeDescriptor) {
    this.startPc = startPc;
    this.endPc = endPc;
    this.handlerPc = handlerPc;
    this.catchType = catchType;
    this.catchTypeDescriptor = catchTypeDescriptor;
  }

  /**
   * Constructs a new Handler from the given one, with a different scope.
   *
   * @param handler an existing Handler.
   * @param startPc the start_pc field of this JVMS exception_table entry.
   * @param endPc the end_pc field of this JVMS exception_table entry.
   */
  Handler(final Handler handler, final Label startPc, final Label endPc) {
    this(startPc, endPc, handler.handlerPc, handler.catchType, handler.catchTypeDescriptor);
    this.nextHandler = handler.nextHandler;
  }

  /**
   * Removes the range between start and end from the Handler list that begins with the given
   * element.
   *
   * @param firstHandler the beginning of a Handler list. May be {@literal null}.
   * @param start the start of the range to be removed.
   * @param end the end of the range to be removed. Maybe {@literal null}.
   * @return the exception handler list with the start-end range removed.
   */
  static Handler removeRange(final Handler firstHandler, final Label start, final Label end) {
    if (firstHandler == null) {
      return null;
    } else {
      firstHandler.nextHandler = removeRange(firstHandler.nextHandler, start, end);
    }
    int handlerStart = firstHandler.startPc.bytecodeOffset;
    int handlerEnd = firstHandler.endPc.bytecodeOffset;
    int rangeStart = start.bytecodeOffset;
    int rangeEnd = end == null ? Integer.MAX_VALUE : end.bytecodeOffset;
    // Return early if [handlerStart,handlerEnd[ and [rangeStart,rangeEnd[ don't intersect.
    if (rangeStart >= handlerEnd || rangeEnd <= handlerStart) {
      return firstHandler;
    }
    if (rangeStart <= handlerStart) {
      if (rangeEnd >= handlerEnd) {
        // If [handlerStart,handlerEnd[ is included in [rangeStart,rangeEnd[, remove firstHandler.
        return firstHandler.nextHandler;
      } else {
        // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = [rangeEnd,handlerEnd[
        return new Handler(firstHandler, end, firstHandler.endPc);
      }
    } else if (rangeEnd >= handlerEnd) {
      // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = [handlerStart,rangeStart[
      return new Handler(firstHandler, firstHandler.startPc, start);
    } else {
      // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ =
      //     [handlerStart,rangeStart[ + [rangeEnd,handerEnd[
      firstHandler.nextHandler = new Handler(firstHandler, end, firstHandler.endPc);
      return new Handler(firstHandler, firstHandler.startPc, start);
    }
  }

  /**
   * Returns the number of elements of the Handler list that begins with the given element.
   *
   * @param firstHandler the beginning of a Handler list. May be {@literal null}.
   * @return the number of elements of the Handler list that begins with 'handler'.
   */
  static int getExceptionTableLength(final Handler firstHandler) {
    int length = 0;
    Handler handler = firstHandler;
    while (handler != null) {
      length++;
      handler = handler.nextHandler;
    }
    return length;
  }

  /**
   * Returns the size in bytes of the JVMS exception_table corresponding to the Handler list that
   * begins with the given element. This includes the exception_table_length field.
   *
   * @param firstHandler the beginning of a Handler list. May be {@literal null}.
   * @return the size in bytes of the exception_table_length and exception_table structures.
   */
  static int getExceptionTableSize(final Handler firstHandler) {
    return 2 + 8 * getExceptionTableLength(firstHandler);
  }

  /**
   * Puts the JVMS exception_table corresponding to the Handler list that begins with the given
   * element. This includes the exception_table_length field.
   *
   * @param firstHandler the beginning of a Handler list. May be {@literal null}.
   * @param output where the exception_table_length and exception_table structures must be put.
   */
  static void putExceptionTable(final Handler firstHandler, final ByteVector output) {
    output.putShort(getExceptionTableLength(firstHandler));
    Handler handler = firstHandler;
    while (handler != null) {
      output
          .putShort(handler.startPc.bytecodeOffset)
          .putShort(handler.endPc.bytecodeOffset)
          .putShort(handler.handlerPc.bytecodeOffset)
          .putShort(handler.catchType);
      handler = handler.nextHandler;
    }
  }
}


// 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.action;

import jodd.madvoc.component.ActionsManager;
import jodd.madvoc.component.FiltersManager;
import jodd.madvoc.component.InterceptorsManager;
import jodd.madvoc.component.ResultsManager;
import jodd.madvoc.config.ActionRuntime;
import jodd.madvoc.filter.ActionFilter;
import jodd.madvoc.interceptor.ActionInterceptor;
import jodd.madvoc.meta.In;
import jodd.madvoc.meta.Out;
import jodd.madvoc.meta.scope.MadvocContext;
import jodd.madvoc.result.ActionResult;

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

/**
 * Helper action that returns sorted list of all registered action runtime configurations,
 * action results and interceptors. It can be extended as an Madvoc action or used independently.
 */
public class MadvocConfigurations {

	@In @MadvocContext
	protected ActionsManager actionsManager;

	@In @MadvocContext
	protected FiltersManager filtersManager;

	@In @MadvocContext
	protected InterceptorsManager interceptorsManager;

	@In @MadvocContext
	protected ResultsManager resultsManager;

	@Out
	protected List<ActionRuntime> actions;

	@Out
	protected List<ActionResult> results;

	@Out
	protected List<ActionInterceptor> interceptors;

	@Out
	protected List<ActionFilter> filters;

	/**
	 * Collects all interceptors.
	 */
	protected void collectActionInterceptors() {
		final Collection<? extends ActionInterceptor> interceptorValues = interceptorsManager.getAllInterceptors();
		interceptors = new ArrayList<>();
		interceptors.addAll(interceptorValues);
		interceptors.sort(Comparator.comparing(a -> a.getClass().getSimpleName()));
	}

	/**
	 * Collects all filters.
	 */
	protected void collectActionFilters() {
		final Collection<? extends ActionFilter> filterValues = filtersManager.getAllFilters();
		filters = new ArrayList<>();
		filters.addAll(filterValues);
		filters.sort(Comparator.comparing(a -> a.getClass().getSimpleName()));
	}

	/**
	 * Collects all action results.
	 */
	protected void collectActionResults() {
		final Collection<ActionResult> resultsValues = resultsManager.getAllActionResults();
		results = new ArrayList<>();
		results.addAll(resultsValues);
		results.sort(Comparator.comparing(a -> a.getClass().getSimpleName()));
	}

	/**
	 * Collects all action runtime configurations.
	 */
	protected void collectActionRuntimes() {
		actions = actionsManager.getAllActionRuntimes();
		actions.sort(Comparator.comparing(ActionRuntime::getActionPath));
	}

}


// 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.bean;

import jodd.introspector.Getter;
import jodd.introspector.Setter;
import jodd.util.ClassUtil;
import jodd.util.StringUtil;

import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Instantiable version of {@link BeanUtil}.
 */
public class BeanUtilBean extends BeanUtilUtil implements BeanUtil {

	/**
	 * Sets the declared flag.
	 */
	public BeanUtilBean declared(final boolean declared) {
		this.isDeclared = declared;
		return this;
	}

	/**
	 * Sets the forced flag.
	 */
	public BeanUtilBean forced(final boolean forced) {
		this.isForced = forced;
		return this;
	}

	/**
	 * Sets the silent flag.
	 */
	public BeanUtilBean silent(final boolean silent) {
		this.isSilent = silent;
		return this;
	}

	// ---------------------------------------------------------------- internal resolver

	/**
	 * Resolves nested property name to the very last indexed property.
	 * If forced, null or non-existing properties will be created.
	 */
	protected void resolveNestedProperties(final BeanProperty bp) {
		String name = bp.name;
		int dotNdx;
		while ((dotNdx = indexOfDot(name)) != -1) {
			bp.last = false;
			bp.setName(name.substring(0, dotNdx));
			bp.updateBean(getIndexProperty(bp));
			name = name.substring(dotNdx + 1);
		}
		bp.last = true;
		bp.setName(name);
	}

	protected boolean resolveExistingNestedProperties(final BeanProperty bp) {
		String name = bp.name;
		int dotNdx;
		while ((dotNdx = indexOfDot(name)) != -1) {
			bp.last = false;
			bp.setName(name.substring(0, dotNdx));
			final String temp = bp.name;
			if (!hasIndexProperty(bp)) {
				return false;
			}
			bp.setName(temp);
			bp.updateBean(getIndexProperty(bp));
			name = name.substring(dotNdx + 1);
		}
		bp.last = true;
		bp.setName(name);
		return true;
	}

	// ---------------------------------------------------------------- simple property

	@Override
	public boolean hasSimpleProperty(final Object bean, final String property) {
		return hasSimpleProperty(new BeanProperty(this, bean, property));
	}

	protected boolean hasSimpleProperty(final BeanProperty bp) {
		if (bp.bean == null) {
			return false;
		}

		// try: getter
		final Getter getter = bp.getGetter(isDeclared);
		if (getter != null) {
			return true;
		}

		// try: (Map) get("property")
		if (bp.isMap()) {
			Map map = (Map) bp.bean;
			if (map.containsKey(bp.name)) {
				return true;
			}
		}

		return false;
	}

	@Override
	public <T> T getSimpleProperty(final Object bean, final String property) {
		return (T) getSimpleProperty(new BeanProperty(this, bean, property));
	}

	protected Object getSimpleProperty(final BeanProperty bp) {

		if (bp.name.length() == 0) {
			if (bp.indexString != null) {
				// index string exist, but property name is missing
				return bp.bean;
			}
			throw new BeanException("Invalid property", bp);
		}

		Getter getter = bp.getGetter(isDeclared);

		if (getter != null) {
			Object result;
			try {
				result = getter.invokeGetter(bp.bean);
			} catch (Exception ex) {
				if (isSilent) {
					return null;
				}
				throw new BeanException("Getter failed: " + getter, ex);
			}

			if ((result == null) && (isForced)) {
				result = createBeanProperty(bp);
			}
			return result;
		}

		// try: (Map) get("property")
		if (bp.isMap()) {
			Map map = (Map) bp.bean;
			Object key = convertIndexToMapKey(getter, bp.name);

			if (!map.containsKey(key)) {
				if (!isForced) {
					if (isSilent) {
						return null;
					}
					throw new BeanException("Map key not found: " + bp.name, bp);
				}
				Map value = new HashMap();
				//noinspection unchecked
				map.put(key, value);
				return value;
			}
			return map.get(key);
		}

		// failed
		if (isSilent) {
			return null;
		}
		throw new BeanException("Simple property not found: " + bp.name, bp);
	}

	@Override
	public void setSimpleProperty(final Object bean, final String property, final Object value) {
		setSimpleProperty(new BeanProperty(this, bean, property), value);
	}

	/**
	 * Sets a value of simple property.
	 */
	@SuppressWarnings({"unchecked"})
	protected void setSimpleProperty(final BeanProperty bp, final Object value) {
		Setter setter = bp.getSetter(isDeclared);

		// try: setter
		if (setter != null) {
			invokeSetter(setter, bp, value);
			return;
		}

		// try: put("property", value)
		if (bp.isMap()) {
			((Map) bp.bean).put(bp.name, value);
			return;
		}
		if (isSilent) {
			return;
		}
		throw new BeanException("Simple property not found: " + bp.name, bp);
	}

	// ---------------------------------------------------------------- indexed property

	protected boolean hasIndexProperty(final BeanProperty bp) {

		if (bp.bean == null) {
			return false;
		}
		String indexString = extractIndex(bp);

		if (indexString == null) {
			return hasSimpleProperty(bp);
		}

		Object resultBean = getSimpleProperty(bp);

		if (resultBean == null) {
			return false;
		}

		// try: property[index]
		if (resultBean.getClass().isArray()) {
			int index = parseInt(indexString, bp);
			return (index >= 0) && (index < Array.getLength(resultBean));
		}

		// try: list.get(index)
		if (resultBean instanceof List) {
			int index = parseInt(indexString, bp);
			return (index >= 0) && (index < ((List)resultBean).size());
		}
		if (resultBean instanceof Map) {
			return ((Map)resultBean).containsKey(indexString);
		}

		// failed
		return false;
	}

	@Override
	public <T> T getIndexProperty(final Object bean, final String property, final int index) {
		BeanProperty bp = new BeanProperty(this, bean, property);

		bp.indexString = bp.index = String.valueOf(index);

		Object value = _getIndexProperty(bp);

		bp.indexString = null;

		return (T) value;
	}

	/**
	 * Get non-nested property value: either simple or indexed property.
	 * If forced, missing bean will be created if possible.
	 */
	protected Object getIndexProperty(final BeanProperty bp) {
		bp.indexString = extractIndex(bp);

		Object value = _getIndexProperty(bp);

		bp.indexString = null;

		return value;
	}

	private Object _getIndexProperty(final BeanProperty bp) {
		Object resultBean = getSimpleProperty(bp);
		Getter getter = bp.getGetter(isDeclared);

		if (bp.indexString == null) {
			return resultBean;	// no index, just simple bean
		}
		if (resultBean == null) {
			if (isSilent) {
				return null;
			}
			throw new BeanException("Index property is null: " + bp.name, bp);
		}

		// try: property[index]
		if (resultBean.getClass().isArray()) {
			int index = parseInt(bp.indexString, bp);
			if (isForced) {
				return arrayForcedGet(bp, resultBean, index);
			} else {
				return Array.get(resultBean, index);
			}
		}

		// try: list.get(index)
		if (resultBean instanceof List) {
			int index = parseInt(bp.indexString, bp);
			List list = (List) resultBean;
			if (!isForced) {
				return list.get(index);
			}
			if (!bp.last) {
				ensureListSize(list, index);
			}
			Object value = list.get(index);
			if (value == null) {
				Class listComponentType = extractGenericComponentType(getter);
				if (listComponentType == Object.class) {
					// not an error: when component type is unknown, use Map as generic bean
					listComponentType = Map.class;
				}
				try {
					value = ClassUtil.newInstance(listComponentType);
				} catch (Exception ex) {
					if (isSilent) {
						return null;
					}
					throw new BeanException("Invalid list element: " + bp.name + '[' + index + ']', bp, ex);
				}
				//noinspection unchecked
				list.set(index, value);
			}
			return value;
		}

		// try: map.get('index')
		if (resultBean instanceof Map) {
			Map map = (Map) resultBean;
			Object key = convertIndexToMapKey(getter, bp.indexString);

			if (!isForced) {
				return map.get(key);
			}
			Object value = map.get(key);
			if (!bp.last) {
				if (value == null) {
					Class mapComponentType = extractGenericComponentType(getter);
					if (mapComponentType == Object.class) {
						mapComponentType = Map.class;
					}
					try {
						value = ClassUtil.newInstance(mapComponentType);
					} catch (Exception ex) {
						if (isSilent) {
							return null;
						}
						throw new BeanException("Invalid map element: " + bp.name + '[' + bp.indexString + ']', bp, ex);
					}

					//noinspection unchecked
					map.put(key, value);
				}
			}
			return value;
		}

		// failed
		if (isSilent) {
			return null;
		}
		throw new BeanException("Index property is not an array, list or map: " + bp.name, bp);
	}

	@Override
	public void setIndexProperty(final Object bean, final String property, final int index, final Object value) {
		BeanProperty bp = new BeanProperty(this, bean, property);

		bp.indexString = bp.index = String.valueOf(index);

		_setIndexProperty(bp, value);

		bp.indexString = null;
	}

	/**
	 * Sets indexed or regular properties (no nested!).
	 */
	protected void setIndexProperty(final BeanProperty bp, final Object value) {
		bp.indexString = extractIndex(bp);

		_setIndexProperty(bp, value);

		bp.indexString = null;
	}

	@SuppressWarnings({"unchecked"})
	private void _setIndexProperty(final BeanProperty bp, Object value) {
		if (bp.indexString == null) {
			setSimpleProperty(bp, value);
			return;
		}

		// try: getInner()
		Object nextBean = getSimpleProperty(bp);
		Getter getter = bp.getGetter(isDeclared);

		if (nextBean == null) {
			if (isSilent) {
				return;
			}
			throw new BeanException("Index property is null:" + bp.name, bp);
		}

		// inner bean found
		if (nextBean.getClass().isArray()) {
			int index = parseInt(bp.indexString, bp);
			if (isForced) {
				arrayForcedSet(bp, nextBean, index, value);
			} else {
				Array.set(nextBean, index, value);
			}
			return;
		}

		if (nextBean instanceof List) {
			int index = parseInt(bp.indexString, bp);
			Class listComponentType = extractGenericComponentType(getter);
			if (listComponentType != Object.class) {
				value = convertType(value, listComponentType);
			}
			List list = (List) nextBean;
			if (isForced) {
				ensureListSize(list, index);
			}
			list.set(index, value);
			return;
		}
		if (nextBean instanceof Map) {
			Map map = (Map) nextBean;
			Object key = convertIndexToMapKey(getter, bp.indexString);

			Class mapComponentType = extractGenericComponentType(getter);
			if (mapComponentType != Object.class) {
				value = convertType(value, mapComponentType);
			}
			map.put(key, value);
			return;
		}

		// failed
		if (isSilent) {
			return;
		}
		throw new BeanException("Index property is not an array, list or map: " + bp.name, bp);
	}

	// ---------------------------------------------------------------- SET

	@Override
	public void setProperty(final Object bean, final String name, final Object value) {
		BeanProperty beanProperty = new BeanProperty(this, bean, name);

		if (!isSilent) {
			resolveNestedProperties(beanProperty);
			setIndexProperty(beanProperty, value);
		}
		else {
			try {
				resolveNestedProperties(beanProperty);
				setIndexProperty(beanProperty, value);
			}
			catch (Exception ignore) {}
		}
	}

	// ---------------------------------------------------------------- GET

	/**
	 * Returns value of bean's property.
	 */
	@Override
	public <T> T getProperty(final Object bean, final String name) {
		BeanProperty beanProperty = new BeanProperty(this, bean, name);
		if (!isSilent) {
			resolveNestedProperties(beanProperty);
			return (T) getIndexProperty(beanProperty);
		}
		else {
			try {
				resolveNestedProperties(beanProperty);
				return (T) getIndexProperty(beanProperty);
			}
			catch (Exception ignore) {
				return null;
			}
		}
	}

	// ---------------------------------------------------------------- HAS

	@Override
	public boolean hasProperty(final Object bean, final String name) {
		BeanProperty beanProperty = new BeanProperty(this, bean, name);
		if (!resolveExistingNestedProperties(beanProperty)) {
			return false;
		}
		return hasIndexProperty(beanProperty);
	}

	@Override
	public boolean hasRootProperty(final Object bean, String name) {
		int dotNdx = indexOfDot(name);
		if (dotNdx != -1) {
			name = name.substring(0, dotNdx);
		}
		BeanProperty beanProperty = new BeanProperty(this, bean, name);
		extractIndex(beanProperty);
		return hasSimpleProperty(beanProperty);
	}

	// ---------------------------------------------------------------- type

	@Override
	public Class<?> getPropertyType(final Object bean, final String name) {
		BeanProperty beanProperty = new BeanProperty(this, bean, name);
		if (!resolveExistingNestedProperties(beanProperty)) {
			return null;
		}
		hasIndexProperty(beanProperty);
		return extractType(beanProperty);
	}

	// ---------------------------------------------------------------- utilities

	private static final char[] INDEX_CHARS = new char[] {'.', '['};

	/**
	 * Extract the first name of this reference.
	 */
	@Override
	public String extractThisReference(final String propertyName) {
		int ndx = StringUtil.indexOfChars(propertyName, INDEX_CHARS);
		if (ndx == -1) {
			return propertyName;
		}
		return propertyName.substring(0, ndx);
	}

}


// 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.servlet.filter;

import jodd.io.FastCharArrayWriter;
import jodd.util.StringPool;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * Response wrapper that takes everything the client would normally output
 * and saves it in character array. It works only for writers, byte stream
 * is not buffered.
 */
public class CharArrayResponseWrapper extends HttpServletResponseWrapper {

	protected FastCharArrayWriter writer;
	protected FastByteArrayServletOutputStream out;
	protected PrintWriter printWriter;

	/**
	 * Initializes wrapper by creating {@link FastCharArrayWriter} that will
	 * be used to accumulate the response.
	 */
	public CharArrayResponseWrapper(final HttpServletResponse response) {
		super(response);
	}

	/**
	 * Returns buffered writer. Buffer will be created if not already used.
	 */
	@Override
	public PrintWriter getWriter() {
		if (writer == null) {
			writer = new FastCharArrayWriter();
			printWriter = new PrintWriter(writer);
		}
		return printWriter;
	}

	@Override
	public ServletOutputStream getOutputStream() throws IOException {
		throw new IOException("Using output stream is not supported");
	}

	/**
	 * Get a String representation of the entire buffer.
	 *

	 * Do not call this method multiple times on the same wrapper as
	 * new string is created every time.
	 */
	@Override
	public String toString() {
		if (writer == null) {
			return StringPool.EMPTY;
		}
		return writer.toString();
	}

	@Override
	public void reset() {
		if (writer != null) {
			writer.reset();
		}
	}

	// ---------------------------------------------------------------- add-on

	/**
	 * Explicitly closes the writer.
	 */
	public void close() {
		if (writer != null) {
			writer.close();
		}
	}

	/**
	 * Returns the size (number of characters) of written data.
	 */
	public int size() {
		if (writer == null) {
			return 0;
		}
		return writer.size();
	}

	/**
	 * Get the underlying character array or null if
	 * writer not used.
	 */
	public char[] toCharArray() {
		if (writer == null) {
			return null;
		}
		return writer.toCharArray();
	}

}


// 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.oom.sqlgen;

import jodd.db.DbOom;
import jodd.db.oom.ColumnAliasType;
import jodd.db.oom.ColumnData;
import jodd.db.oom.DbEntityColumnDescriptor;
import jodd.db.oom.DbEntityDescriptor;
import jodd.db.oom.DbEntityManager;
import jodd.db.oom.NamedValuesHashMap;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Template common data used during template parsing.
 * Extracted to super class for better visibility.
 */
public abstract class TemplateData {

	private static final String COL_CODE_PREFIX = "col_";
	protected final DbEntityManager entityManager;
	protected final ColumnAliasType defaultColumnAliasType;

	protected TemplateData(final DbOom dbOom) {
		this.entityManager = dbOom.entityManager();
		this.columnAliasType = defaultColumnAliasType = dbOom.config().getDefaultColumnAliasType();
	}

	/**
	 * Resets the builder so it can be used again. Not everything is reset:
	 * object references and column alias type is not.
	 */
	protected void resetSoft() {
		columnCount = 0;
		paramCount = 0;
		hintCount = 0;

		if (tableRefs != null) {
			tableRefs.clear();
		}
//		objectRefs = null;
		if (columnData != null) {
			columnData.clear();
		}
		if (parameters != null) {
			parameters.clear();
		}
		if (hints != null) {
			hints.clear();
		}
		//columnAliasType = defaultColumnAliasType;
	}

	protected void resetHard() {
		resetSoft();
		objectRefs = null;
		columnAliasType = defaultColumnAliasType;
	}

	// ---------------------------------------------------------------- object refs

	protected Map<String, Object> objectRefs;             // used object references

	/**
	 * Saves object reference.
	 */
	public void setObjectReference(final String name, final Object object) {
		if (objectRefs == null) {
			objectRefs = new HashMap<>();
		}
		objectRefs.put(name, object);
	}

	/**
	 * Returns object reference.
	 */
	public Object getObjectReference(final String name) {
		if (objectRefs == null) {
			return null;
		}
		return objectRefs.get(name);
	}

	/**
	 * Lookups for object reference and throws an exception if reference doesn't exist.
	 */
	public Object lookupObject(final String ref) {
		Object value = getObjectReference(ref);
		if (value == null) {
			throw new DbSqlBuilderException("Invalid object reference: " + ref);
		}
		return value;
	}

	// ---------------------------------------------------------------- tables

	private static final class TableRefData {
		final String alias;
		final DbEntityDescriptor desc;

		private TableRefData(final DbEntityDescriptor desc, final String alias) {
			this.alias = alias;
			this.desc = desc;
		}
	}

	protected Map<String, TableRefData> tableRefs;

	/**
	 * Returns entity descriptor for provided table reference.
	 */
	public DbEntityDescriptor getTableDescriptor(final String tableRef) {
		if (tableRefs == null) {
			return null;
		}
		TableRefData t = tableRefs.get(tableRef);
		return t == null ? null : t.desc;
	}

	/**
	 * Finds entity descriptor of a table that contains provided column reference.
	 */
	public DbEntityDescriptor findTableDescriptorByColumnRef(final String columnRef) {
		for (Map.Entry<String, TableRefData> entry : tableRefs.entrySet()) {
			DbEntityDescriptor ded = entry.getValue().desc;

			if (ded.findByPropertyName(columnRef) != null) {
				return ded;
			}
		}
		return null;
	}

	/**
	 * Returns table alias for provided table reference.
	 */
	public String getTableAlias(final String tableRef) {
		if (tableRefs == null) {
			return null;
		}
		TableRefData t = tableRefs.get(tableRef);
		return t == null ? null : t.alias;
	}

	/**
	 * Registers table reference for provided entity.
	 */
	public void registerTableReference(final String tableReference, final DbEntityDescriptor ded, final String tableAlias) {
		if (tableRefs == null) {
			tableRefs = new HashMap<>();
		}
		TableRefData t = new TableRefData(ded, tableAlias);
		if (tableRefs.put(tableReference, t) != null) {
			throw new DbSqlBuilderException("Duplicated table reference: " + tableReference);
		}
	}

	// ---------------------------------------------------------------- column data

	/**
	 * Column or table aliases.
	 */
	protected Map<String, ColumnData> columnData;

	/**
	 * Column counter for COLUMN_CODE column alias type.
	 */
	protected int columnCount;

	/**
	 * Specifies column alias type. May be null when column aliases are not used.
	 */
	protected ColumnAliasType columnAliasType;

	/**
	 * Returns column alias type.
	 */
	public ColumnAliasType getColumnAliasType() {
		return columnAliasType;
	}

	public void registerColumnDataForTableRef(final String tableRef, final String tableName) {
		if (columnData == null) {
			columnData = new NamedValuesHashMap<>();
		}
		columnData.put(tableRef, new ColumnData(tableName));
	}

	public String registerColumnDataForColumnCode(final String tableName, final String column) {
		if (columnData == null) {
			columnData = new NamedValuesHashMap<>();
		}
		String columnCode = COL_CODE_PREFIX + Integer.toString(columnCount++) + '_';
		columnData.put(columnCode, new ColumnData(tableName, column));
		return columnCode;
	}

	// ---------------------------------------------------------------- parameters

	protected Map<String, ParameterValue> parameters;

	protected int paramCount;

	/**
	 * Returns the next auto-generated parameter name.
	 */
	public String getNextParameterName() {
		return "p" + (paramCount++);
	}

	/**
	 * Adds query parameter.
	 */
	public void addParameter(final String name, final Object value, final DbEntityColumnDescriptor dec) {
		if (parameters == null) {
			parameters = new HashMap<>();
		}
		parameters.put(name, new ParameterValue(value, dec));
	}

	// ---------------------------------------------------------------- lookup

	/**
	 * Lookups for entity name and throws exception if entity name not found.
	 */
	protected DbEntityDescriptor lookupName(final String entityName) {
		DbEntityDescriptor ded = entityManager.lookupName(entityName);
		if (ded == null) {
			throw new DbSqlBuilderException("Entity name not registered: " + entityName);
		}
		return ded;
	}

	/**
	 * Lookups for entity name and throws an exception if entity type is invalid.
	 */
	protected DbEntityDescriptor lookupType(final Class entity) {
		DbEntityDescriptor ded = entityManager.lookupType(entity);
		if (ded == null) {
			throw new DbSqlBuilderException("Invalid or not-persistent entity type: " + entity.getName());
		}
		return ded;
	}

	/**
	 * Lookups for table reference and throws an exception if table reference not found.
	 */
	protected DbEntityDescriptor lookupTableRef(final String tableRef) {
		DbEntityDescriptor ded = getTableDescriptor(tableRef);
		if (ded == null) {
			throw new DbSqlBuilderException("Table reference not used in this query: " + tableRef);
		}
		return ded;
	}

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

	/**
	 * Defines parameter with name and its value.
	 */
	protected void defineParameter(final StringBuilder query, String name, final Object value) {
		if (name == null) {
			name = getNextParameterName();
		}
		query.append(':').append(name);
		addParameter(name, value, null);
	}

	// ---------------------------------------------------------------- hints

	protected int hintCount;

	protected List<String> hints;

	/**
	 * Registers a hint.
	 */
	public void registerHint(final String hint) {
		if (hints == null) {
			hints = new ArrayList<>(hintCount);
		}
		hints.add(hint);
	}

	/**
	 * Increments hints count.
	 */
	public void incrementHintsCount() {
		hintCount++;
	}

	/**
	 * Returns true if there are hints.
	 */
	public boolean hasHints() {
		return hintCount > 0;
	}

	// ---------------------------------------------------------------- last column

	public DbEntityColumnDescriptor lastColumnDec;

}


// 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.impl;

import jodd.json.JsonContext;
import jodd.json.TypeJsonSerializer;

/**
 * Base class for all JSON objects and arrays serializers.
 * Detects circular dependencies and pushes value to current type
 * context of provided json context.
 */
public abstract class ValueJsonSerializer<T> implements TypeJsonSerializer<T> {

	/**
	 * Detects circular dependencies and pushes value as current
	 * type context.
	 */
	@Override
	public final boolean serialize(final JsonContext jsonContext, final T value) {
		if (jsonContext.pushValue(value)) {
			// prevent circular dependencies
			return false;
		}

		serializeValue(jsonContext, value);

		jsonContext.popValue();

		return true;
	}

	/**
	 * Performs the serialization of the value.
	 */
	public abstract void serializeValue(JsonContext jsonContext, T 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.madvoc.scope;

import jodd.io.upload.FileUpload;
import jodd.madvoc.ActionRequest;
import jodd.madvoc.component.MadvocEncoding;
import jodd.madvoc.config.Targets;
import jodd.petite.meta.PetiteInject;
import jodd.servlet.ServletUtil;
import jodd.servlet.upload.MultipartRequestWrapper;
import jodd.util.StringPool;
import jodd.util.StringUtil;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

/**
 * The request scope.
 */
public class RequestScope extends RequestScopeCfg implements MadvocScope {

	@PetiteInject
	MadvocEncoding madvocEncoding;

	protected final ActionPathMacroInjector actionPathMacroInjector = new ActionPathMacroInjector(this);
	protected final InstancesInjector instancesInjector = new InstancesInjector(this);

	// ---------------------------------------------------------------- inject

	@Override
	public void inject(final ActionRequest actionRequest, final Targets targets) {
		final HttpServletRequest servletRequest = actionRequest.getHttpServletRequest();

		instancesInjector.inject(actionRequest, targets);

		if (injectAttributes) {
			injectAttributes(servletRequest, targets);
		}
		if (injectParameters) {
			injectParameters(servletRequest, targets);
			injectUploadedFiles(servletRequest, targets);
		}

		actionPathMacroInjector.inject(actionRequest, targets);
	}

	@Override
	public void inject(final ServletContext servletContext, final Targets targets) {

	}

	@Override
	public void inject(final Targets targets) {

	}

	/**
	 * Injects request attributes.
	 */
	protected void injectAttributes(final HttpServletRequest servletRequest, final Targets targets) {
		final Enumeration<String> attributeNames = servletRequest.getAttributeNames();
		while (attributeNames.hasMoreElements()) {
			final String attrName = attributeNames.nextElement();

			targets.forEachTargetAndIn(this, (target, in) -> {
				final String name = in.matchedName(attrName);
				if (name != null) {
					final Object attrValue = servletRequest.getAttribute(attrName);
					target.writeValue(name, attrValue, true);
				}
			});
		}
	}

	/**
	 * Inject request parameters.
	 */
	protected void injectParameters(final HttpServletRequest servletRequest, final Targets targets) {
		final boolean encode = encodeGetParams && servletRequest.getMethod().equals("GET");
		final Enumeration<String> paramNames = servletRequest.getParameterNames();

		while (paramNames.hasMoreElements()) {
			final String paramName = paramNames.nextElement();
			if (servletRequest.getAttribute(paramName) != null) {
				continue;
			}

			targets.forEachTargetAndIn(this, (target, in) -> {
				final String name = in.matchedName(paramName);
				if (name != null) {
					String[] paramValues = servletRequest.getParameterValues(paramName);

					paramValues = ServletUtil.prepareParameters(
						paramValues, treatEmptyParamsAsNull, ignoreEmptyRequestParams);

					if (paramValues != null) {
						if (encode) {
							for (int j = 0; j < paramValues.length; j++) {
								final String p = paramValues[j];
								if (p != null) {
									final String encoding = madvocEncoding.getEncoding();
									paramValues[j] = StringUtil.convertCharset(p, StringPool.ISO_8859_1, encoding);
								}
							}
						}
						final Object value = (paramValues.length != 1 ? paramValues : paramValues[0]);
						target.writeValue(name, value, true);
					}
				}
			});
		}
	}

	/**
	 * Inject uploaded files from multipart request parameters.
	 */
	protected void injectUploadedFiles(final HttpServletRequest servletRequest, final Targets targets) {
		if (!(servletRequest instanceof MultipartRequestWrapper)) {
			return;
		}
		final MultipartRequestWrapper multipartRequest = (MultipartRequestWrapper) servletRequest;
		if (!multipartRequest.isMultipart()) {
			return;
		}
		final Enumeration<String> paramNames = multipartRequest.getFileParameterNames();
		while (paramNames.hasMoreElements()) {
			final String paramName = paramNames.nextElement();
			if (servletRequest.getAttribute(paramName) != null) {
				continue;
			}

			targets.forEachTargetAndIn(this, (target, in) -> {
				final String name = in.matchedName(paramName);
				if (name != null) {
					final FileUpload[] paramValues = multipartRequest.getFiles(paramName);

					if (ignoreInvalidUploadFiles) {
						for (int j = 0; j < paramValues.length; j++) {
							final FileUpload paramValue = paramValues[j];

							if ((!paramValue.isValid()) || (!paramValue.isUploaded())) {
								paramValues[j] = null;
							}
						}
					}

					final Object value = (paramValues.length == 1 ? paramValues[0] : paramValues);
					target.writeValue(name, value, true);
				}
			});
		}
	}

	// ---------------------------------------------------------------- outject

	@Override
	public void outject(final ActionRequest actionRequest, final Targets targets) {
		final HttpServletRequest servletRequest = actionRequest.getHttpServletRequest();

		targets.forEachTargetAndOut(this, (target, out) -> {
			final Object value = target.readValue(out);
			servletRequest.setAttribute(out.name(), 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.db.oom.naming;

import jodd.util.Format;

/**
 * Naming strategy for converting property names
 * to database column names.
 */
public class ColumnNamingStrategy extends BaseNamingStrategy {

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

	/**
	 * Converts property name to column name.
	 */
	public String convertPropertyNameToColumnName(final String propertyName) {
		StringBuilder tableName = new StringBuilder(propertyName.length() * 2);

		if (splitCamelCase) {
			String convertedTableName = Format.fromCamelCase(propertyName, separatorChar);
			tableName.append(convertedTableName);
		} else {
			tableName.append(propertyName);
		}

		if (!changeCase) {
			return tableName.toString();
		}
		return uppercase ?
				toUppercase(tableName).toString() :
				toLowercase(tableName).toString();

	}

	/**
	 * Converts column name to property name.
	 */
	public String convertColumnNameToPropertyName(final String columnName) {
		StringBuilder propertyName = new StringBuilder(columnName.length());
		int len = columnName.length();

		if (splitCamelCase) {
			boolean toUpper = false;
			for (int i = 0; i < len; i++) {
				char c = columnName.charAt(i);
				if (c == separatorChar) {
					toUpper = true;
					continue;
				}
				if (toUpper) {
					propertyName.append(Character.toUpperCase(c));
					toUpper = false;
				} else {
					propertyName.append(Character.toLowerCase(c));
				}
			}
			return propertyName.toString();
		}
		return columnName;
	}

	/**
	 * Applies column naming strategy to given column name hint.
	 * Returns full column name.
	 */
	public String applyToColumnName(final String columnName) {
		String propertyName = convertColumnNameToPropertyName(columnName);

		return convertPropertyNameToColumnName(propertyName);
	}

}


// 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.StringUtil;

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

/**
 * Converts given object to boolean[].
 */
public class BooleanArrayConverter implements TypeConverter<boolean[]> {

	protected final TypeConverterManager typeConverterManager;

	public BooleanArrayConverter(final TypeConverterManager typeConverterManager) {
		this.typeConverterManager = typeConverterManager;
	}

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

		final 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 boolean convertType(final Object value) {
		return typeConverterManager.convertType(value, boolean.class).booleanValue();
	}

	/**
	 * Creates an array with single element.
	 */
	protected boolean[] convertToSingleElementArray(final Object value) {
		return new boolean[] {convertType(value)};
	}

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

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

		if (value instanceof Iterable) {
			final Iterable iterable = (Iterable) value;

			final ArrayList<Boolean> booleanArrayList = new ArrayList<>();

			for (final Object element : iterable) {
				final boolean convertedValue = convertType(element);
				booleanArrayList.add(Boolean.valueOf(convertedValue));
            }

			final boolean[] array = new boolean[booleanArrayList.size()];

			for (int i = 0; i < booleanArrayList.size(); i++) {
				final Boolean b = booleanArrayList.get(i);
				array[i] = b.booleanValue();
			}

			return array;
		}

		if (value instanceof CharSequence) {
			final String[] strings = StringUtil.splitc(value.toString(), ArrayConverter.NUMBER_DELIMITERS);
			return convertArrayToArray(strings);
		}

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

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

		final boolean[] result;

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

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

		return result;
	}

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

		if (primitiveComponentType == boolean.class) {
			return (boolean[]) value;
		}

		if (primitiveComponentType == int.class) {
			final int[] array = (int[]) value;
			result = new boolean[array.length];
			for (int i = 0; i < array.length; i++) {
				result[i] = array[i] != 0;
			}
		}
		else if (primitiveComponentType == long.class) {
			final long[] array = (long[]) value;
			result = new boolean[array.length];
			for (int i = 0; i < array.length; i++) {
				result[i] = array[i] != 0;
			}
		}
		else if (primitiveComponentType == float.class) {
			final float[] array = (float[]) value;
			result = new boolean[array.length];
			for (int i = 0; i < array.length; i++) {
				result[i] = array[i] != 0;
			}
		}
		else if (primitiveComponentType == double.class) {
			final double[] array = (double[]) value;
			result = new boolean[array.length];
			for (int i = 0; i < array.length; i++) {
				result[i] = array[i] != 0;
			}
		}
		else if (primitiveComponentType == short.class) {
			final short[] array = (short[]) value;
			result = new boolean[array.length];
			for (int i = 0; i < array.length; i++) {
				result[i] = array[i] != 0;
			}
		}
		else if (primitiveComponentType == byte.class) {
			final byte[] array = (byte[]) value;
			result = new boolean[array.length];
			for (int i = 0; i < array.length; i++) {
				result[i] = array[i] != 0;
			}
		}
		else if (primitiveComponentType == char.class) {
			final char[] array = (char[]) value;
			result = new boolean[array.length];
			for (int i = 0; i < array.length; i++) {
				result[i] = array[i] != 0;
			}
		}
		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.db.type;

import jodd.typeconverter.TypeConverterManager;
import jodd.util.ClassUtil;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * SQL type.
 */
public abstract class SqlType<T> {

	/**
	 * Indicator for not yet resolved DB SQL type.
	 */
	public static final int DB_SQLTYPE_UNKNOWN = Integer.MAX_VALUE;

	/**
	 * Indicator for unavailable DB SQL type. Used usually when sql type is not
	 * available by JDBC meta data or when column can not be matched
	 * (due to case-mismatching).
	 */
	public static final int DB_SQLTYPE_NOT_AVAILABLE = Integer.MIN_VALUE;

	protected Class<T> sqlType;

	@SuppressWarnings({"unchecked"})
	protected SqlType() {
		this.sqlType = ClassUtil.getGenericSupertype(this.getClass(), 0);
	}

	/**
	 * Sets prepared statement value.
	 */
	public abstract void set(PreparedStatement st, int index, T value, int dbSqlType) throws SQLException;

	/**
	 * Returns value from result set.
	 * @param rs result set
	 * @param index column index
	 * @param dbSqlType java.sql.Types hint
	 */
	public abstract T get(ResultSet rs, int index, int dbSqlType) throws SQLException;

	/**
	 * Stores value in database. Value is casted to sql type.
	 */
	public void storeValue(final PreparedStatement st, final int index, final Object value, final int dbSqlType) throws SQLException {
		T t = TypeConverterManager.get().convertType(value, sqlType);
		set(st, index, t, dbSqlType);
	}

	/**
	 * Reads value from database. Value is casted to destination type.
	 * @param rs result set
	 * @param index database column index
	 * @param destinationType property type
	 * @param dbSqlType hint for column sql type value
	 */
	public <E> E readValue(final ResultSet rs, final int index, final Class<E> destinationType, final int dbSqlType) throws SQLException {
		T t = get(rs, index, dbSqlType);
		return prepareGetValue(t, destinationType);
	}

	/**
	 * Once when value is read from result set, prepare it to match destination type.
	 * @param t get value
	 * @param destinationType destination type
	 */
	@SuppressWarnings({"unchecked"})
	protected <E> E prepareGetValue(final T t, final Class<E> destinationType) {
		if (t == null) {
			return null;
		}
		if (destinationType == null) {
			return (E) t;
		}
		return TypeConverterManager.get().convertType(t, destinationType);
	}

}


// 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.component;

import jodd.madvoc.ActionWrapper;
import jodd.madvoc.BaseActionWrapperStack;
import jodd.madvoc.MadvocException;
import jodd.petite.meta.PetiteInject;
import jodd.util.ClassUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Base wrapper manager implements common logic of a wrapper.
 */
public abstract class WrapperManager<T extends ActionWrapper> {

	@PetiteInject
	protected ContextInjectorComponent contextInjectorComponent;

	protected WrapperManager() {
		wrappers = new HashMap<>();
	}

	// ---------------------------------------------------------------- container

	protected final Map<String, T> wrappers;

	/**
	 * Returns all action wrappers. Returns a copy in new set.
	 */
	protected Set<T> getAll() {
		final Set<T> set = new HashSet<>(wrappers.size());
		set.addAll(wrappers.values());
		return set;
	}

	/**
	 * Looks up for existing wrapper. Returns null if wrapper is not already registered.
	 */
	public T lookup(final String name) {
		return wrappers.get(name);
	}

	/**
	 * Resolves single wrapper. Creates new wrapper instance if not already registered.
	 * Does not expand the wrappers.
	 */
	public T resolve(final Class<? extends T> wrapperClass) {
		String wrapperClassName = wrapperClass.getName();

		T wrapper = lookup(wrapperClassName);

		if (wrapper == null) {
			wrapper = createWrapper(wrapperClass);

			initializeWrapper(wrapper);

			wrappers.put(wrapperClassName, wrapper);
		}
		return wrapper;
	}

	/**
	 * Resolves wrappers. Unregistered wrappers will be registered. Returned array may be
	 * different size than size of provided array, due to {@link #expand(Class[]) expanding}.
	 */
	public T[] resolveAll(Class<? extends T>[] wrapperClasses) {
		if (wrapperClasses == null) {
			return null;
		}
		wrapperClasses = expand(wrapperClasses);
		T[] result = createArray(wrapperClasses.length);

		for (int i = 0; i < wrapperClasses.length; i++) {
			result[i] = resolve(wrapperClasses[i]);
		}
		return result;
	}

	/**
	 * Creates an array of wrapper instances.
	 */
	protected abstract T[] createArray(int len);

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

	/**
	 * Initializes action wrapper.
	 */
	protected void initializeWrapper(final T wrapper) {
		contextInjectorComponent.injectContext(wrapper);

		wrapper.init();
	}

	// ---------------------------------------------------------------- expander

	/**
	 * Replaces all {@link BaseActionWrapperStack} with stack values.
	 */
  protected Class<? extends T>[] expand(final Class<? extends T>[] actionWrappers) {
		if (actionWrappers == null) {
			return null;
		}
		List<Class<? extends T>> list = new ArrayList<>(actionWrappers.length);
		list.addAll(Arrays.asList(actionWrappers));

		int i = 0;
		while (i < list.size()) {
			Class<? extends T> wrapperClass = list.get(i);
			if (wrapperClass == null) {
				continue;
			}
			if (ClassUtil.isTypeOf(wrapperClass, BaseActionWrapperStack.class)) {
				BaseActionWrapperStack stack = (BaseActionWrapperStack) resolve(wrapperClass);
				list.remove(i);
				Class<? extends T>[] stackWrappers = stack.getWrappers();
				if (stackWrappers != null) {
					list.addAll(i, Arrays.asList(stackWrappers));
				}
				i--;
				//continue;
			}
			i++;
		}
		return list.toArray(new Class[0]);
	}

	// ---------------------------------------------------------------- create

	/**
	 * Creates new wrapper.
	 */
	protected <R extends T> R createWrapper(final Class<R> wrapperClass) {
		try {
		    return ClassUtil.newInstance(wrapperClass);
		} catch (Exception ex) {
			throw new MadvocException("Invalid Madvoc wrapper: " + wrapperClass, ex);
		}
	}

}


// 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.io;

import jodd.system.SystemUtil;
import jodd.util.StringPool;

import java.io.File;

/**
 * General filename and filepath manipulation utilities.
 *

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

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

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

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

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

 *
the prefix - C:\

 *
the path - dev\project\

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

 *
the name - file.txt

 *
the base name - file

 *
the extension - txt

 *

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

 * This class only supports Unix and Windows style names.
 * Prefixes are matched as follows:
 *
{@code
 * Windows:
 * a\b\c.txt           --> ""          --> relative
 * \a\b\c.txt          --> "\"         --> current drive absolute
 * C:a\b\c.txt         --> "C:"        --> drive relative
 * C:\a\b\c.txt        --> "C:\"       --> absolute
 * \\server\a\b\c.txt  --> "\\server\" --> UNC
 *
 * Unix:
 * a/b/c.txt           --> ""          --> relative
 * /a/b/c.txt          --> "/"         --> absolute
 * ~/a/b/c.txt         --> "~/"        --> current user
 * ~                   --> "~/"        --> current user (slash added)
 * ~user/a/b/c.txt     --> "~user/"    --> named user
 * ~user               --> "~user/"    --> named user (slash added)
 * }

 * Both prefix styles are matched always, irrespective of the machine that you are
 * currently running on.
 */
public class FileNameUtil {

	/**
	 * The extension separator character.
	 */
	private static final char EXTENSION_SEPARATOR = '.';

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

	/**
	 * The Windows separator character.
	 */
	private static final char WINDOWS_SEPARATOR = '\\';

	/**
	 * The system separator character.
	 */
	private static final char SYSTEM_SEPARATOR = File.separatorChar;

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

	/**
	 * Checks if the character is a separator.
	 */
	private static boolean isSeparator(final char ch) {
		return (ch == UNIX_SEPARATOR) || (ch == WINDOWS_SEPARATOR);
	}

	// ---------------------------------------------------------------- normalization

	public static String normalize(final String filename) {
		return doNormalize(filename, SYSTEM_SEPARATOR, true);
	}

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

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

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

	 * The output will be the same on both Unix and Windows except
	 * for the separator character.
	 *
{@code
	 * /foo//               -->   /foo/
	 * /foo/./              -->   /foo/
	 * /foo/../bar          -->   /bar
	 * /foo/../bar/         -->   /bar/
	 * /foo/../bar/../baz   -->   /baz
     * //foo//./bar         -->   /foo/bar
	 * /../                 -->   null
	 * ../foo               -->   null
	 * foo/bar/..           -->   foo/
	 * foo/../../bar        -->   null
	 * foo/../bar           -->   bar
	 * //server/foo/../bar  -->   //server/bar
	 * //server/../bar      -->   null
	 * C:\foo\..\bar        -->   C:\bar
	 * C:\..\bar            -->   null
	 * ~/foo/../bar/        -->   ~/bar/
	 * ~/../bar             -->   null
	 * }

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

	public static String normalizeNoEndSeparator(final String filename) {
		return doNormalize(filename, SYSTEM_SEPARATOR, false);
	}

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

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

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

	 * The output will be the same on both Unix and Windows except
	 * for the separator character.
	 *
{@code
	 * /foo//               -->   /foo
	 * /foo/./              -->   /foo
	 * /foo/../bar          -->   /bar
	 * /foo/../bar/         -->   /bar
	 * /foo/../bar/../baz   -->   /baz
	 * /foo//./bar          -->   /foo/bar
	 * /../                 -->   null
	 * ../foo               -->   null
	 * foo/bar/..           -->   foo
	 * foo/../../bar        -->   null
	 * foo/../bar           -->   bar
	 * //server/foo/../bar  -->   //server/bar
	 * //server/../bar      -->   null
	 * C:\foo\..\bar        -->   C:\bar
	 * C:\..\bar            -->   null
	 * ~/foo/../bar/        -->   ~/bar
	 * ~/../bar             -->   null
	 * }

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

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

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

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

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

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

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

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

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

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

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

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

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

	 * The output will be the same on both Unix and Windows except
	 * for the separator character.
	 *
{@code
	 * /foo/ + bar          -->   /foo/bar
	 * /foo + bar           -->   /foo/bar
	 * /foo + /bar          -->   /bar
	 * /foo + C:/bar        -->   C:/bar
	 * /foo + C:bar         -->   C:bar (*)
	 * /foo/a/ + ../bar     -->   foo/bar
	 * /foo/ + ../../bar    -->   null
	 * /foo/ + /bar         -->   /bar
	 * /foo/.. + /bar       -->   /bar
	 * /foo + bar/c.txt     -->   /foo/bar/c.txt
	 * /foo/c.txt + bar     -->   /foo/c.txt/bar (!)
	 * }

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

	// ---------------------------------------------------------------- separator conversion

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

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

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

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

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

	 * The prefix length includes the first slash in the full filename
	 * if applicable. Thus, it is possible that the length returned is greater
	 * than the length of the input string.
	 *
{@code
	 * Windows:
	 * a\b\c.txt           --> ""          --> relative
	 * \a\b\c.txt          --> "\"         --> current drive absolute
	 * C:a\b\c.txt         --> "C:"        --> drive relative
	 * C:\a\b\c.txt        --> "C:\"       --> absolute
	 * \\server\a\b\c.txt  --> "\\server\" --> UNC
	 *
	 * Unix:
	 * a/b/c.txt           --> ""          --> relative
	 * /a/b/c.txt          --> "/"         --> absolute
	 * ~/a/b/c.txt         --> "~/"        --> current user
	 * ~                   --> "~/"        --> current user (slash added)
	 * ~user/a/b/c.txt     --> "~user/"    --> named user
	 * ~user               --> "~user/"    --> named user (slash added)
	 * }

	 *

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

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

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

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

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

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

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

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

	/**
	 * Returns true if file has extension.
	 */
	public static boolean hasExtension(final String filename) {
		return indexOfExtension(filename) != -1;
	}

	// ---------------------------------------------------------------- get

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

	 * This method will handle a file in either Unix or Windows format.
	 * The prefix includes the first slash in the full filename where applicable.
	 *
{@code
	 * Windows:
	 * a\b\c.txt           --> ""          --> relative
	 * \a\b\c.txt          --> "\"         --> current drive absolute
	 * C:a\b\c.txt         --> "C:"        --> drive relative
	 * C:\a\b\c.txt        --> "C:\"       --> absolute
	 * \\server\a\b\c.txt  --> "\\server\" --> UNC
	 *
	 * Unix:
	 * a/b/c.txt           --> ""          --> relative
	 * /a/b/c.txt          --> "/"         --> absolute
	 * ~/a/b/c.txt         --> "~/"        --> current user
	 * ~                   --> "~/"        --> current user (slash added)
	 * ~user/a/b/c.txt     --> "~user/"    --> named user
	 * ~user               --> "~user/"    --> named user (slash added)
	 * }

	 *

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

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

	 * This method will handle a file in either Unix or Windows format.
	 * The method is entirely text based, and returns the text before and
	 * including the last forward or backslash.
	 *
{@code
	 * C:\a\b\c.txt --> a\b\
	 * ~/a/b/c.txt  --> a/b/
	 * a.txt        --> ""
	 * a/b/c        --> a/b/
	 * a/b/c/       --> a/b/c/
	 * }

	 *

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

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

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

	 * This method will handle a file in either Unix or Windows format.
	 * The method is entirely text based, and returns the text before the
	 * last forward or backslash.
	 *
{@code
	 * C:\a\b\c.txt --> a\b
	 * ~/a/b/c.txt  --> a/b
	 * a.txt        --> ""
	 * a/b/c        --> a/b
	 * a/b/c/       --> a/b/c
	 * }

	 *

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

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

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

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

	 * This method will handle a file in either Unix or Windows format.
	 * The method is entirely text based, and returns the text before and
	 * including the last forward or backslash.
	 *
{@code
	 * C:\a\b\c.txt --> C:\a\b\
	 * ~/a/b/c.txt  --> ~/a/b/
	 * a.txt        --> ""
	 * a/b/c        --> a/b/
	 * a/b/c/       --> a/b/c/
	 * C:           --> C:
	 * C:\          --> C:\
	 * ~            --> ~/
	 * ~/           --> ~/
	 * ~user        --> ~user/
	 * ~user/       --> ~user/
	 * }

	 *

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

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

	 * This method will handle a file in either Unix or Windows format.
	 * The method is entirely text based, and returns the text before the
	 * last forward or backslash.
	 *
{@code
	 * C:\a\b\c.txt --> C:\a\b
	 * ~/a/b/c.txt  --> ~/a/b
	 * a.txt        --> ""
	 * a/b/c        --> a/b
	 * a/b/c/       --> a/b/c
	 * C:           --> C:
	 * C:\          --> C:\
	 * ~            --> ~
	 * ~/           --> ~
	 * ~user        --> ~user
	 * ~user/       --> ~user
	 * }

	 *

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

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

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

	 * This method will handle a file in either Unix or Windows format.
	 * The text after the last forward or backslash is returned.
	 *
{@code
	 * a/b/c.txt --> c.txt
	 * a.txt     --> a.txt
	 * a/b/c     --> c
	 * a/b/c/    --> ""
	 * }

	 *

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

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

	 * This method will handle a file in either Unix or Windows format.
	 * The text after the last forward or backslash and before the last dot is returned.
	 *
{@code
	 * a/b/c.txt --> c
	 * a.txt     --> a
	 * a/b/c     --> c
	 * a/b/c/    --> ""
	 * }

	 *

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

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

	 * This method returns the textual part of the filename after the last dot.
	 * There must be no directory separator after the dot.
	 *
{@code
	 * foo.txt      --> "txt"
	 * a/b/c.jpg    --> "jpg"
	 * a/b.txt/c    --> ""
	 * a/b/c        --> ""
	 * }

	 *

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

	//----------------------------------------------------------------------- remove

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

	 * This method returns the textual part of the filename before the last dot.
	 * There must be no directory separator after the dot.
	 *
{@code
	 * foo.txt    --> foo
	 * a\b\c.jpg  --> a\b\c
	 * a\b\c      --> a\b\c
	 * a.b\c      --> a.b\c
	 * }

	 *

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

	// ---------------------------------------------------------------- equals

	/**
	 * Checks whether two filenames are equal exactly.
	 */
	public static boolean equals(final String filename1, final String filename2) {
		return equals(filename1, filename2, false);
	}

	/**
	 * Checks whether two filenames are equal using the case rules of the system.
	 */
	public static boolean equalsOnSystem(final String filename1, final String filename2) {
		return equals(filename1, filename2, true);
	}

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

	 *
	 * @param filename1  the first filename to query, may be null
	 * @param filename2  the second filename to query, may be null
	 * @param system  whether to use the system (windows or unix)
	 * @return true if the filenames are equal, null equals null
	 */
	private static boolean equals(final String filename1, final String filename2, final boolean system) {
		//noinspection StringEquality
		if (filename1 == filename2) {
			return true;
		}
		if (filename1 == null || filename2 == null) {
			return false;
		}
		if (system && (SYSTEM_SEPARATOR == WINDOWS_SEPARATOR)) {
			return filename1.equalsIgnoreCase(filename2);
		} else {
			return filename1.equals(filename2);
		}
	}

	// ---------------------------------------------------------------- split

	/**
	 * Splits filename into a array of four Strings containing prefix, path, basename and extension.
	 * Path will contain ending separator.
	 */
	public static String[] split(final String filename) {
		String prefix = getPrefix(filename);
		if (prefix == null) {
			prefix = StringPool.EMPTY;
		}
		int lastSeparatorIndex = indexOfLastSeparator(filename);
		int lastExtensionIndex = indexOfExtension(filename);

		String path;
		String baseName;
		String extension;

		if (lastSeparatorIndex == -1) {
			path = StringPool.EMPTY;
			if (lastExtensionIndex == -1) {
				baseName = filename.substring(prefix.length());
				extension = StringPool.EMPTY;
			} else {
				baseName = filename.substring(prefix.length(), lastExtensionIndex);
				extension = filename.substring(lastExtensionIndex + 1);
			}
		} else {
			path = filename.substring(prefix.length(), lastSeparatorIndex + 1);
			if (lastExtensionIndex == -1) {
				baseName = filename.substring(prefix.length() + path.length());
				extension = StringPool.EMPTY;
			} else {
				baseName = filename.substring(prefix.length() + path.length(), lastExtensionIndex);
				extension = filename.substring(lastExtensionIndex + 1);
			}
		}
		return new String[] {prefix, path, baseName, extension};
	}

	// ---------------------------------------------------------------- home

	/**
	 * Resolve ~ in the path.
	 */
	public static String resolveHome(final String path) {
		if (path.length() == 1) {
			if (path.charAt(0) == '~') {
				return SystemUtil.info().getHomeDir();
			}
			return path;
		}
		if (path.length() >= 2) {
			if ((path.charAt(0) == '~') && (path.charAt(1) == File.separatorChar)) {
				return SystemUtil.info().getHomeDir() + path.substring(1);
			}
		}
		return path;
	}

	/**
	 * Calculates relative path of target path on base path.
	 */
	public static String relativePath(final String targetPath, final String basePath) {
		return new File(basePath).toPath().relativize(new File(targetPath).toPath()).toString();
	}

}