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

import jodd.chalk.Chalk256;
import jodd.joy.madvoc.JoyActionConfig;
import jodd.joy.madvoc.JoyRestActionConfig;
import jodd.madvoc.AutomagicMadvocConfigurator;
import jodd.madvoc.WebApp;
import jodd.madvoc.component.ActionConfigManager;
import jodd.madvoc.component.ActionsManager;
import jodd.madvoc.config.ActionRuntime;
import jodd.madvoc.meta.Action;
import jodd.madvoc.meta.RestAction;
import jodd.madvoc.petite.PetiteWebApp;
import jodd.madvoc.proxetta.ProxettaAwareActionsManager;
import jodd.madvoc.proxetta.ProxettaSupplier;
import jodd.props.Props;
import jodd.proxetta.ProxettaUtil;
import jodd.util.ClassUtil;
import jodd.util.function.Consumers;

import javax.servlet.ServletContext;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * Tiny JoyMadvoc kickstarter. It is a special component, as it runs last and performs
 * the classpath scanning.
 */
public class JoyMadvoc extends JoyBase {

	private final Supplier<String> appNameSupplier;
	private final Supplier<JoyProxetta> joyProxettaSupplier;
	private final Supplier<JoyPetite> joyPetiteSupplier;
	private final Supplier<JoyScanner> joyScannerSupplier;
	private final Supplier<JoyProps> joyPropsSupplier;
	private final Consumers<WebApp> webAppConsumers;

	private ServletContext servletContext;
	private PetiteWebApp webApp;
	private Supplier<PetiteWebApp> webAppSupplier;

	public JoyMadvoc(
			final Supplier<String> appNameSupplier,
			final Supplier<JoyPetite> joyPetiteSupplier,
			final Supplier<JoyProxetta> joyProxettaSupplier,
			final Supplier<JoyProps> joyPropsSupplier,
			final Supplier<JoyScanner> joyScannerSupplier) {
		this.appNameSupplier = appNameSupplier;
		this.joyProxettaSupplier = joyProxettaSupplier;
		this.joyPetiteSupplier = joyPetiteSupplier;
		this.joyScannerSupplier = joyScannerSupplier;
		this.joyPropsSupplier = joyPropsSupplier;
		this.webAppConsumers = Consumers.empty();
	}

	/**
	 * Defines a web app supplier that creates custom {@link PetiteWebApp}.
	 */
	public void setWebAppSupplier(final Supplier<PetiteWebApp> webAppSupplier) {
		this.webAppSupplier = webAppSupplier;
	}

	/**
	 * Defines optional servlet context.
	 */
	public void setServletContext(final ServletContext servletContext) {
		this.servletContext = servletContext;
	}

	public void add(final Consumer<WebApp> webAppConsumer) {
		this.webAppConsumers.add(webAppConsumer);
	}

	// ---------------------------------------------------------------- runtime

	public WebApp getWebApp() {
		return webApp;
	}

	// ---------------------------------------------------------------- lifecycle

	@Override
	public void start() {
		initLogger();

		log.info("MADVOC start  ----------");

		webApp = webAppSupplier == null ? new PetiteWebApp(joyPetiteSupplier.get().getPetiteContainer()) : webAppSupplier.get();

		webApp.withRegisteredComponent(ActionConfigManager.class, acm -> {
			acm.bindAnnotationConfig(Action.class, JoyActionConfig.class);
			acm.bindAnnotationConfig(RestAction.class, JoyRestActionConfig.class);
		});

		if (servletContext != null) {
			webApp.bindServletContext(servletContext);
		}

		final Props allProps = joyPropsSupplier.get().getProps();

		webApp.withParams(allProps.innerMap(beanNamePrefix()));

		webApp.registerComponent(new ProxettaSupplier(joyProxettaSupplier.get().getProxetta()));
		webApp.registerComponent(ProxettaAwareActionsManager.class);

		// Automagic Madvoc configurator will scan and register ALL!
		// This way we reduce the startup time and have only one scanning.
		// Scanning happens in the INIT phase.
		final AutomagicMadvocConfigurator automagicMadvocConfigurator =
			new AutomagicMadvocConfigurator(joyScannerSupplier.get().getClassScanner()) {
				@Override
				protected String createInfoMessage() {
					return "Scanning completed in " + elapsed + "ms.";
				}
			};

		webApp.registerComponent(automagicMadvocConfigurator);

		webAppConsumers.accept(webApp);

		webApp.start();

		log.info("MADVOC OK!");
	}

	protected String beanNamePrefix() {
		final String appName = appNameSupplier.get();
		return appName + ".madvoc.";
	}

	@Override
	public void stop() {
		if (log != null) {
			log.info("MADVOC stop  ----------");
		}
		if (webApp != null) {
			webApp.shutdown();
		}
		webApp = null;
	}

	// ---------------------------------------------------------------- print

	/**
	 * Prints routes to console.
	 */
	protected void printRoutes(final int width) {
		final ActionsManager actionsManager = webApp.madvocContainer().lookupComponent(ActionsManager.class);
		final List<ActionRuntime> actions = actionsManager.getAllActionRuntimes();
		final Map<String, String> aliases = actionsManager.getAllAliases();

		if (actions.isEmpty()) {
			return;
		}

		final Print print = new Print();

		print.line("Routes", width);

		actions.stream()
			.sorted(Comparator.comparing(
				actionRuntime -> actionRuntime.getActionPath() + ' ' + actionRuntime.getActionMethod()))
			.forEach(ar -> {

				final String actionMethod = ar.getActionMethod();

				print.out(Chalk256.chalk().yellow(), actionMethod == null ? "*" : actionMethod, 7);
				print.space();

				final String signature =
					ClassUtil.getShortClassName(
						ProxettaUtil.resolveTargetClass(ar.getActionClass()), 2)
						+ '#' + ar.getActionClassMethod().getName();

				print.outLeftRightNewLine(
					Chalk256.chalk().green(), ar.getActionPath(),
					Chalk256.chalk().blue(), signature,
					width - 7 - 1
				);
			});

		if (!aliases.isEmpty()) {

			print.line("Aliases", width);

			actions.stream()
				.sorted(Comparator.comparing(
					actionRuntime -> actionRuntime.getActionPath() + ' ' + actionRuntime.getActionMethod()))
				.forEach(ar -> {

					final String actionPath = ar.getActionPath();

					for (final Map.Entry<String, String> entry : aliases.entrySet()) {
						if (entry.getValue().equals(actionPath)) {
							print.space(8);

							print.outLeftRightNewLine(
								Chalk256.chalk().green(), entry.getValue(),
								Chalk256.chalk().blue(), entry.getKey(),
								width - 8
							);
						}
					}
				});
		}

		print.line(width);
	}

}


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

import jodd.buffer.FastByteBuffer;

/**
 * Generic block chipher.
 */
public abstract class BlockCipher {

	protected static final byte TERMINATOR = -1;

	protected final int blockSizeInBytes;

	protected BlockCipher(final int blockSize) {
		this.blockSizeInBytes = blockSize >> 3;
	}

	/**
	 * Encrypts single block of plain content. It ensures that there is
	 * always a block size amount of data.
	 */
	protected abstract byte[] encryptBlock(byte[] content, int offset);

	/**
	 * Decrypts single block of encrypted content.
	 */
	protected abstract byte[] decryptBlock(byte[] encryptedContent, int offset);

	/**
	 * Encrypts complete content, block by block.
	 */
	public byte[] encrypt(final byte[] content) {
		FastByteBuffer fbb = new FastByteBuffer();

		int length = content.length + 1;
		int blockCount = length / blockSizeInBytes;
		int remaining = length;

		int offset = 0;
		for (int i = 0; i < blockCount; i++) {
			if (remaining == blockSizeInBytes) {
				break;
			}
			byte[] encrypted = encryptBlock(content, offset);

			fbb.append(encrypted);

			offset += blockSizeInBytes;
			remaining -= blockSizeInBytes;
		}

		if (remaining != 0) {
			// process remaining bytes
			byte[] block = new byte[blockSizeInBytes];

			System.arraycopy(content, offset, block, 0, remaining - 1);

			block[remaining - 1] = TERMINATOR;

			byte[] encrypted = encryptBlock(block, 0);

			fbb.append(encrypted);
		}

		return fbb.toArray();
	}

	/**
	 * Decrypts the whole content, block by block.
	 */
	public byte[] decrypt(final byte[] encryptedContent) {
		FastByteBuffer fbb = new FastByteBuffer();

		int length = encryptedContent.length;
		int blockCount = length / blockSizeInBytes;

		int offset = 0;
		for (int i = 0; i < blockCount - 1; i++) {
			byte[] decrypted = decryptBlock(encryptedContent, offset);

			fbb.append(decrypted);

			offset += blockSizeInBytes;
		}

		// process last block
		byte[] decrypted = decryptBlock(encryptedContent, offset);

		// find terminator
		int ndx = blockSizeInBytes - 1;

		while (ndx >= 0) {
			if (decrypted[ndx] == TERMINATOR) {
				break;
			}
			ndx--;
		}

		fbb.append(decrypted, 0, ndx);

		return fbb.toArray();
	}

}


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

package jodd.json;

import jodd.introspector.ClassDescriptor;
import jodd.introspector.ClassIntrospector;
import jodd.introspector.PropertyDescriptor;
import jodd.introspector.Setter;
import jodd.typeconverter.TypeConverterManager;
import jodd.util.ClassLoaderUtil;
import jodd.util.ClassUtil;
import jodd.util.Wildcard;

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

/**
 * Map to bean converter.
 * Used when parsing with class metadata enabled.
 */
public class MapToBean {

	protected boolean declared = true;
	protected final JsonParserBase jsonParser;
	protected final String classMetadataName;

	public MapToBean(final JsonParserBase jsonParser, final String classMetadataName) {
		this.jsonParser = jsonParser;
		this.classMetadataName = classMetadataName;
	}

	/**
	 * Converts map to target type.
	 */
	public Object map2bean(final Map map, Class targetType) {
		Object target = null;

		// create targets type
		String className = (String) map.get(classMetadataName);

		if (className == null) {
			if (targetType == null) {
				// nothing to do, no information about target type found
				target = map;
			}
		}
		else {
			checkClassName(jsonParser.classnameWhitelist, className);

			try {
				targetType = ClassLoaderUtil.loadClass(className);
			} catch (ClassNotFoundException cnfex) {
				throw new JsonException(cnfex);
			}
		}

		if (target == null) {
			target = jsonParser.newObjectInstance(targetType);
		}

		ClassDescriptor cd = ClassIntrospector.get().lookup(target.getClass());

		boolean targetIsMap = target instanceof Map;

		for (Object key : map.keySet()) {
			String keyName = key.toString();

			if (classMetadataName != null) {
				if (keyName.equals(classMetadataName)) {
					continue;
				}
			}

			PropertyDescriptor pd = cd.getPropertyDescriptor(keyName, declared);

			if (!targetIsMap && pd == null) {
				// target property does not exist, continue
				continue;
			}

			// value is one of JSON basic types, like Number, Map, List...
			Object value = map.get(key);

			Class propertyType = pd == null ? null : pd.getType();
			Class componentType = pd == null ? null : pd.resolveComponentType(true);

			if (value != null) {
				if (value instanceof List) {
					if (componentType != null && componentType != String.class) {
						value = generifyList((List) value, componentType);
					}
				}
				else if (value instanceof Map) {
					// if the value we want to inject is a Map...
					if (!ClassUtil.isTypeOf(propertyType, Map.class)) {
						// ... and if target is NOT a map
						value = map2bean((Map) value, propertyType);
					}
					else {
						// target is also a Map, but we might need to generify it
						Class keyType = pd == null ? null : pd.resolveKeyType(true);

						if (keyType != String.class || componentType != String.class) {
							// generify
							value = generifyMap((Map) value, keyType, componentType);
						}
					}
				}
			}

			if (targetIsMap) {
				((Map)target).put(keyName, value);
			}
			else {
				try {
					setValue(target, pd, value);
				} catch (Exception ignore) {
					ignore.printStackTrace();
				}
			}
		}

		return target;
	}

	private void checkClassName(final List<String> classnameWhitelist, final String className) {
		if (classnameWhitelist == null) {
			return;
		}
		classnameWhitelist.forEach(pattern -> {
			if (!Wildcard.equalsOrMatch(className, pattern)) {
				throw new JsonException("Class can't be loaded as it is not whitelisted: " + className);
			}
		});
	}

	/**
	 * Converts type of all list elements to match the component type.
	 */
	private Object generifyList(final List list, final Class componentType) {
		for (int i = 0; i < list.size(); i++) {
			Object element = list.get(i);

			if (element != null) {
				if (element instanceof Map) {
					Object bean = map2bean((Map) element, componentType);
					list.set(i, bean);
				} else {
					Object value = convert(element, componentType);
					list.set(i, value);
				}
			}
		}

		return list;
	}

	/**
	 * Sets the property value.
	 */
	private void setValue(final Object target, final PropertyDescriptor pd, Object value) throws InvocationTargetException, IllegalAccessException {
		Class propertyType;

		Setter setter = pd.getSetter(true);
		if (setter != null) {
			if (value != null) {
				propertyType = setter.getSetterRawType();
				value = jsonParser.convertType(value, propertyType);
			}
			setter.invokeSetter(target, value);
		}
	}

	/**
	 * Change map elements to match key and value types.
	 */
	protected <K,V> Map<K, V> generifyMap(final Map<Object, Object> map, final Class<K> keyType, final Class<V> valueType) {

		if (keyType == String.class) {
			// only value type is changed, we can make value replacements
			for (Map.Entry<Object, Object> entry : map.entrySet()) {
				Object value = entry.getValue();
				Object newValue = convert(value, valueType);

				if (value != newValue) {
					entry.setValue(newValue);
				}
			}
			return (Map<K, V>) map;
		}

		// key is changed too, we need a new map
		Map<K, V> newMap = new HashMap<>(map.size());

		for (Map.Entry<Object, Object> entry : map.entrySet()) {
			Object key = entry.getKey();
			Object newKey = convert(key, keyType);

			Object value = entry.getValue();
			Object newValue = convert(value, valueType);

			newMap.put((K)newKey, (V)newValue);
		}

		return newMap;
	}

	protected Object convert(final Object value, final Class targetType) {
		Class valueClass = value.getClass();

		if (valueClass == targetType) {
			return value;
		}

		if (value instanceof Map) {
			if (targetType == Map.class) {
				return value;
			}

			return map2bean((Map) value, targetType);
		}

		try {
			return TypeConverterManager.get().convertType(value, targetType);
		}
		catch (Exception ex) {
			throw new JsonException("Type conversion failed", 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.methref;

import jodd.proxetta.ProxyAdvice;
import jodd.proxetta.ProxyTarget;

import static jodd.proxetta.ProxyTarget.returnType;
import static jodd.proxetta.ProxyTarget.targetMethodName;

/**
 * Methref advice applied on all methods. It puts method name in
 * class variable that can be accessed later using reflection.
 */
public class MethrefAdvice implements ProxyAdvice {

	String methodName;

	/**
	 * Reads method name and stores it in local variable.
	 * For methods that return String returns the method name,
	 * otherwise returns null.
	 */
	public Object execute() {
		methodName = targetMethodName();

		Class returnType = returnType();

		if (returnType == String.class) {
			return ProxyTarget.returnValue(targetMethodName());
		}
		return ProxyTarget.returnValue(null);
	}

}


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

package jodd.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 short[].
 */
public class ShortArrayConverter implements TypeConverter<short[]> {

	protected final TypeConverterManager typeConverterManager;

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

	@Override
	public short[] 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 short convertType(final Object value) {
		return typeConverterManager.convertType(value, short.class).shortValue();
	}

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

	/**
	 * Converts non-array value to array. Detects various
	 * collection types and iterates them to make conversion
	 * and to create target array.
 	 */
	protected short[] convertValueToArray(final Object value) {
		if (value instanceof Collection) {
			final Collection collection = (Collection) value;
			final short[] target = new short[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<Short> shortArrayList = new ArrayList<>();

			for (final Object element : iterable) {
				final short convertedValue = convertType(element);
				shortArrayList.add(Short.valueOf(convertedValue));
			}

			final short[] array = new short[shortArrayList.size()];

			for (int i = 0; i < shortArrayList.size(); i++) {
				final Short s = shortArrayList.get(i);
				array[i] = s.shortValue();
			}

			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 short[] convertArrayToArray(final Object value) {
		final Class valueComponentType = value.getClass().getComponentType();

		final short[] result;

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

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

		return result;
	}

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

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

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

import jodd.db.type.SqlType;
import jodd.util.StringUtil;

/**
 * Column descriptors.
 */
public class DbEntityColumnDescriptor implements Comparable {

	protected final DbEntityDescriptor dbEntityDescriptor;
	protected final String columnName;
	protected final String columnNameForQuery;
	protected final String propertyName;
	protected final Class propertyType;
	protected final boolean isId;
	protected final Class<? extends SqlType> sqlTypeClass;
	protected int dbSqlType = SqlType.DB_SQLTYPE_UNKNOWN;

	public DbEntityColumnDescriptor(
			final DbEntityDescriptor ded,
			final String columnName,
			final String fieldName,
			final Class fieldType,
			final boolean isId,
			final Class<? extends SqlType> sqlTypeClass) {

		this.dbEntityDescriptor = ded;
		this.propertyName = fieldName;
		this.propertyType = fieldType;
		this.isId = isId;
		this.sqlTypeClass = sqlTypeClass;

		this.columnNameForQuery = columnName;

		if (StringUtil.detectQuoteChar(columnNameForQuery) != 0) {
			this.columnName = StringUtil.substring(columnNameForQuery, 1, -1);
		}
		else {
			this.columnName = columnNameForQuery;
		}
	}

	/**
	 * Returns {@link jodd.db.oom.DbEntityDescriptor} that this column description belongs to.
	 */
	public DbEntityDescriptor getDbEntityDescriptor() {
		return dbEntityDescriptor;
	}

	/**
	 * Returns column name.
	 */
	public String getColumnName() {
		return columnName;
	}

	/**
	 * Returns column name to be used in the generated sql.
	 */
	public String getColumnNameForQuery() {
		return columnNameForQuery;
	}

	/**
	 * Returns property name.
	 */
	public String getPropertyName() {
		return propertyName;
	}

	/**
	 * Returns property type.
	 */
	public Class getPropertyType() {
		return propertyType;
	}

	/**
	 * Returns true if column is ID column.
	 */
	public boolean isId() {
		return isId;
	}

	/**
	 * Returns SqlType or null for default type.
	 */
	public Class<? extends SqlType> getSqlTypeClass() {
		return sqlTypeClass;
	}

	/**
	 * Returns db sql type. Important: available only after first saving of column value
	 * to database!
	 */
	public int getDbSqlType() {
		return dbSqlType;
	}

	/**
	 * Updates db sql type if not already set.
	 */
	public void updateDbSqlType(final int dbSqlType) {
		if (this.dbSqlType == SqlType.DB_SQLTYPE_UNKNOWN) {
			this.dbSqlType = dbSqlType;
		}
	}

	// ---------------------------------------------------------------- comparable

	/**
	 * Compares two column descriptors. Identity columns should be the first on the list.
	 * Each group then will be sorted by column name.
	 */
	@Override
	public int compareTo(final Object o) {
		DbEntityColumnDescriptor that = (DbEntityColumnDescriptor) o;
		if (this.isId != that.isId) {
			return this.isId ? -1 : 1;      // IDs should be the first in the array
		}
		return this.columnName.compareTo(that.columnName);
	}

	// ---------------------------------------------------------------- equals and hash

	@Override
	public boolean equals(final Object o) {
		if (this == o) {
			return true;
		}
		if (this.getClass() != o.getClass()) {
			return false;
		}
		DbEntityColumnDescriptor that = (DbEntityColumnDescriptor) o;
		return columnName.equals(that.columnName);
	}

	@Override
	public int hashCode() {
		return columnName.hashCode();
	}

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

	@Override
	public String toString() {
		return "DbEntityColumnDescriptor{" +
				"columnName='" + columnName + '\'' +
				", propertyName='" + propertyName + '\'' +
				", isId=" + isId +
				'}';
	}
}


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

import jodd.exception.ExceptionUtil;

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

/**
 * Emulates HTTP Browser and persist cookies between requests.
 */
public class HttpBrowser {

	protected HttpConnectionProvider httpConnectionProvider;
	protected HttpRequest httpRequest;
	protected HttpResponse httpResponse;
	protected HttpMultiMap<Cookie> cookies = HttpMultiMap.newCaseInsensitiveMap();
	protected HeadersMultiMap defaultHeaders = new HeadersMultiMap();
	protected boolean keepAlive;
	protected long elapsedTime;
	protected boolean catchTransportExceptions = true;

	public HttpBrowser() {
		httpConnectionProvider = HttpConnectionProvider.get();
	}

	/**
	 * Returns true if keep alive is used.
	 */
	public boolean isKeepAlive() {
		return keepAlive;
	}

	/**
	 * Defines that persistent HTTP connection should be used.
	 */
	public HttpBrowser setKeepAlive(final boolean keepAlive) {
		this.keepAlive = keepAlive;
		return this;
	}

	/**
	 * Defines if transport exceptions should be thrown.
	 */
	public HttpBrowser setCatchTransportExceptions(final boolean catchTransportExceptions) {
		this.catchTransportExceptions = catchTransportExceptions;
		return this;
	}

	/**
	 * Defines proxy for a browser.
	 */
	public HttpBrowser setProxyInfo(final ProxyInfo proxyInfo) {
		httpConnectionProvider.useProxy(proxyInfo);
		return this;
	}

	/**
	 * Defines {@link jodd.http.HttpConnectionProvider} for this browser session.
	 * Resets the previous proxy definition, if set.
	 */
	public HttpBrowser setHttpConnectionProvider(final HttpConnectionProvider httpConnectionProvider) {
		this.httpConnectionProvider = httpConnectionProvider;
		return this;
	}

	/**
	 * Adds default header to all requests.
	 */
	public HttpBrowser setDefaultHeader(final String name, final String value) {
		defaultHeaders.addHeader(name, value);
		return this;
	}

	/**
	 * Returns last used request.
	 */
	public HttpRequest getHttpRequest() {
		return httpRequest;
	}

	/**
	 * Returns last received {@link HttpResponse HTTP response} object.
	 */
	public HttpResponse getHttpResponse() {
		return httpResponse;
	}

	/**
	 * Returns last response HTML page.
	 */
	public String getPage() {
		if (httpResponse == null) {
			return null;
		}
		return httpResponse.bodyText();
	}

	/**
	 * Sends new request as a browser. Before sending,
	 * all browser cookies are added to the request.
	 * After sending, the cookies are read from the response.
	 * Moreover, status codes 301 and 302 are automatically
	 * handled. Returns very last response.
	 */
	public HttpResponse sendRequest(HttpRequest httpRequest) {
		elapsedTime = System.currentTimeMillis();

		// send request

		httpRequest.followRedirects(false);

		while (true) {
			this.httpRequest = httpRequest;
			HttpResponse previousResponse = this.httpResponse;
			this.httpResponse = null;

			addDefaultHeaders(httpRequest);
			addCookies(httpRequest);

			// send request
			if (catchTransportExceptions) {
				try {
					this.httpResponse = _sendRequest(httpRequest, previousResponse);
				}
				catch (HttpException httpException) {
					httpResponse = new HttpResponse();
					httpResponse.assignHttpRequest(httpRequest);
					httpResponse.statusCode(503);
					httpResponse.statusPhrase("Service unavailable. " + ExceptionUtil.message(httpException));
				}
			}
			else {
				this.httpResponse =_sendRequest(httpRequest, previousResponse);
			}

			readCookies(httpResponse);

			int statusCode = httpResponse.statusCode();

			// 301: moved permanently
			if (statusCode == 301) {
				String newPath = httpResponse.location();

				if (newPath == null) {
					break;
				}

				httpRequest = HttpRequest.get(newPath);
				continue;
			}

			// 302: redirect, 303: see other
			if (statusCode == 302 || statusCode == 303) {
				String newPath = httpResponse.location();

				if (newPath == null) {
					break;
				}

				httpRequest = HttpRequest.get(newPath);
				continue;
			}

			// 307: temporary redirect, 308: permanent redirect
			if (statusCode == 307 || statusCode == 308) {
				String newPath = httpResponse.location();

				if (newPath == null) {
					break;
				}

				String originalMethod = httpRequest.method();
				httpRequest = new HttpRequest()
						.method(originalMethod)
						.set(newPath);
				continue;
			}

			break;
		}

		elapsedTime = System.currentTimeMillis() - elapsedTime;

		return this.httpResponse;
	}

	/**
	 * Opens connection and sends a response.
	 */
	protected HttpResponse _sendRequest(final HttpRequest httpRequest, final HttpResponse previouseResponse) {
		if (!keepAlive) {
			httpRequest.open(httpConnectionProvider);
		} else {
			// keeping alive
			if (previouseResponse == null) {
				httpRequest.open(httpConnectionProvider).connectionKeepAlive(true);
			} else {
				httpRequest.keepAlive(previouseResponse, true);
			}
		}

		return httpRequest.send();
	}

	/**
	 * Add default headers to the request. If request already has a header set,
	 * default header will be ignored.
	 */
	protected void addDefaultHeaders(final HttpRequest httpRequest) {
		for (Map.Entry<String, String> entry : defaultHeaders.entries()) {
			String name = entry.getKey();

			if (!httpRequest.headers.contains(name)) {
				httpRequest.headers.add(name, entry.getValue());
			}
		}
	}

	/**
	 * Returns elapsed time of last {@link #sendRequest(HttpRequest)} in milliseconds.
	 */
	public long getElapsedTime() {
		return elapsedTime;
	}

	// ---------------------------------------------------------------- close

	/**
	 * Closes browser explicitly, needed when keep-alive connection is used.
	 */
	public void close() {
		if (httpResponse != null) {
			httpResponse.close();
		}
	}

	// ---------------------------------------------------------------- cookies

	/**
	 * Deletes all cookies.
	 */
	public void clearCookies() {
		cookies.clear();
	}

	/**
	 * Reads cookies from response and adds to cookies list.
	 */
	protected void readCookies(final HttpResponse httpResponse) {
		Cookie[] newCookies = httpResponse.cookies();

		for (Cookie cookie : newCookies) {
			cookies.add(cookie.getName(), cookie);
		}
	}

	/**
	 * Add cookies to the request.
	 */
	protected void addCookies(final HttpRequest httpRequest) {
		// prepare all cookies
		List<Cookie> cookiesList = new ArrayList<>();

		if (!cookies.isEmpty()) {
			for (Map.Entry<String, Cookie> cookieEntry : cookies) {
				cookiesList.add(cookieEntry.getValue());
			}

			httpRequest.cookies(cookiesList.toArray(new Cookie[0]));
		}
	}
}


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

import jodd.util.StringPool;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Encapsulates {@link Email} sending session. Prepares and sends {@link #sendMail(Email)} {@link Email}s.
 */
public class SendMailSession extends MailSession<Transport> {

	private static final String ALTERNATIVE = "alternative";
	private static final String RELATED = "related";
	private static final String CHARSET = ";charset=";
	private static final String INLINE = "inline";

	static {
		setupSystemMailProperties();
	}

	/**
	 * Creates new mail session.
	 *
	 * @param session   {@link Session}
	 * @param transport {@link Transport}
	 */
	public SendMailSession(final Session session, final Transport transport) {
		super(session, transport);
	}

	@Override
	public Transport getService() {
		return (Transport) service;
	}

	/**
	 * Prepares message and sends it. Returns Message ID of sent email.
	 *
	 * @param email {@link Email} to send.
	 * @return String representing message ID.
	 */
	public String sendMail(final Email email) {
		try {
			final MimeMessage msg = createMessage(email);
			getService().sendMessage(msg, msg.getAllRecipients());
			return msg.getMessageID();
		} catch (final MessagingException msgexc) {
			throw new MailException("Failed to send email: " + email, msgexc);
		}
	}

	// ---------------------------------------------------------------- adapter

	/**
	 * Creates new {@link MimeMessage} from an {@link Email}.
	 *
	 * @param email {@link Email} to be created as a {@link MimeMessage}.
	 * @return {@link MimeMessage} created from an {@link Email}.
	 * @throws MessagingException if there is a failure
	 */
	protected MimeMessage createMessage(final Email email) throws MessagingException {
		final Email clone = email.clone();

		final MimeMessage newMsg = new MimeMessage(getSession());

		setPeople(clone, newMsg);
		setSubject(clone, newMsg);
		setSentDate(clone, newMsg);
		setHeaders(clone, newMsg);
		addBodyData(clone, newMsg);
		return newMsg;
	}

	/**
	 * Sets subject in msgToSet from subject in emailWithData.
	 *
	 * @param emailWithData {@link Email} with data
	 * @param msgToSet      {@link MimeMessage} to set data into.
	 * @throws MessagingException if there is a failure
	 */
	private void setSubject(final Email emailWithData, final MimeMessage msgToSet) throws MessagingException {
		if (emailWithData.subjectEncoding() != null) {
			msgToSet.setSubject(emailWithData.subject(), emailWithData.subjectEncoding());
		} else {
			msgToSet.setSubject(emailWithData.subject());
		}
	}

	/**
	 * Sets sent date in msgToSet with sent date from emailWithData.
	 *
	 * @param emailWithData {@link Email} with data
	 * @param msgToSet      {@link MimeMessage} to set data into.
	 * @throws MessagingException if there is a failure
	 */
	private void setSentDate(final Email emailWithData, final MimeMessage msgToSet) throws MessagingException {
		Date date = emailWithData.sentDate();
		if (date == null) {
			date = new Date();
		}
		msgToSet.setSentDate(date);
	}

	/**
	 * Sets headers in msgToSet with headers from emailWithData.
	 *
	 * @param emailWithData {@link Email} with data
	 * @param msgToSet      {@link MimeMessage} to set data into.
	 * @throws MessagingException if there is a failure
	 */
	private void setHeaders(final Email emailWithData, final MimeMessage msgToSet) throws MessagingException {
		final Map<String, String> headers = emailWithData.headers();
		if (headers != null) {
			for (final Map.Entry<String, String> entry : headers.entrySet()) {
				msgToSet.setHeader(entry.getKey(), entry.getValue());
			}
		}
	}

	/**
	 * Sets FROM, REPLY-TO and recipients.
	 *
	 * @param emailWithData {@link Email} with data
	 * @param msgToSet      {@link MimeMessage} to set data into.
	 * @throws MessagingException if there is a failure
	 */
	private void setPeople(final Email emailWithData, final MimeMessage msgToSet) throws MessagingException {
		msgToSet.setFrom(emailWithData.from().toInternetAddress());
		msgToSet.setReplyTo(EmailAddress.convert(emailWithData.replyTo()));
		setRecipients(emailWithData, msgToSet);
	}

	/**
	 * Sets TO, CC and BCC in msgToSet with TO, CC and BCC from emailWithData.
	 *
	 * @param emailWithData {@link Email} with data
	 * @param msgToSet      {@link MimeMessage} to set data into.
	 * @throws MessagingException if there is a failure.
	 */
	private void setRecipients(final Email emailWithData, final MimeMessage msgToSet) throws MessagingException {
		// TO
		final InternetAddress[] to = EmailAddress.convert(emailWithData.to());
		if (to.length > 0) {
			msgToSet.setRecipients(RecipientType.TO, to);
		}

		// CC
		final InternetAddress[] cc = EmailAddress.convert(emailWithData.cc());
		if (cc.length > 0) {
			msgToSet.setRecipients(RecipientType.CC, cc);
		}

		// BCC
		final InternetAddress[] bcc = EmailAddress.convert(emailWithData.bcc());
		if (bcc.length > 0) {
			msgToSet.setRecipients(RecipientType.BCC, bcc);
		}
	}

	/**
	 * Adds message data and attachments.
	 *
	 * @param emailWithData {@link Email} with data
	 * @param msgToSet      {@link MimeMessage} to set data into.
	 * @throws MessagingException if there is a failure.
	 */
	private void addBodyData(final Email emailWithData, final MimeMessage msgToSet) throws MessagingException {
		final List<EmailMessage> messages = emailWithData.messages();

		final int totalMessages = messages.size();

		// Need to use new list since filterEmbeddedAttachments(List) removes attachments from the source List
		final List<EmailAttachment<? extends DataSource>> attachments = new ArrayList<>(emailWithData.attachments());

		if (attachments.isEmpty() && totalMessages == 1) {
			// special case: no attachments and just one content
			setContent(messages.get(0), msgToSet);

		} else {
			final MimeMultipart multipart = new MimeMultipart();

			final MimeMultipart msgMultipart = new MimeMultipart(ALTERNATIVE);
			multipart.addBodyPart(getBaseBodyPart(msgMultipart));
			for (final EmailMessage emailMessage : messages) {
				msgMultipart.addBodyPart(getBodyPart(emailMessage, attachments));
			}

			addAnyAttachments(attachments, multipart);

			msgToSet.setContent(multipart);
		}
	}

	/**
	 * Returns new {@link MimeBodyPart} with content set as msgMultipart.
	 *
	 * @param msgMultipart {@link MimeMultipart} to add to the new {@link MimeBodyPart}.
	 * @return new {@link MimeBodyPart} with content set as msgMultipart.
	 * @throws MessagingException if there is a failure.
	 */
	private MimeBodyPart getBaseBodyPart(final MimeMultipart msgMultipart) throws MessagingException {
		final MimeBodyPart bodyPart = new MimeBodyPart();
		bodyPart.setContent(msgMultipart);
		return bodyPart;
	}

	/**
	 * @param emailMessage {@link EmailMessage} with data.
	 * @param attachments  {@link List} of {@link EmailAttachment}s.
	 * @return new {@link MimeBodyPart} with data from emailMessage and attachments.
	 * @throws MessagingException if there is a failure.
	 */
	private MimeBodyPart getBodyPart(final EmailMessage emailMessage, final List<EmailAttachment<? extends DataSource>> attachments) throws MessagingException {

		final MimeBodyPart bodyPart = new MimeBodyPart();

		// detect embedded attachments
		final List<EmailAttachment<? extends DataSource>> embeddedAttachments = filterEmbeddedAttachments(attachments, emailMessage);

		if (embeddedAttachments.isEmpty()) {
			// no embedded attachments, just add message
			setContent(emailMessage, bodyPart);
		} else {
			attachments.removeAll(embeddedAttachments);

			// embedded attachments detected, join them as related
			final MimeMultipart relatedMultipart = new MimeMultipart(RELATED);

			final MimeBodyPart messageData = new MimeBodyPart();

			setContent(emailMessage, messageData);

			relatedMultipart.addBodyPart(messageData);

			addAnyAttachments(embeddedAttachments, relatedMultipart);

			bodyPart.setContent(relatedMultipart);
		}

		return bodyPart;
	}

	/**
	 * Sets emailWithData content into msgToSet.
	 *
	 * @param emailWithData {@link EmailMessage} with data.
	 * @param partToSet     {@link Part} to set data into.
	 * @throws MessagingException if there is a failure.
	 */
	private void setContent(final EmailMessage emailWithData, final Part partToSet) throws MessagingException {
		partToSet.setContent(emailWithData.getContent(), emailWithData.getMimeType() + CHARSET + emailWithData.getEncoding());
	}

	/**
	 * Creates attachment body part. Handles regular and inline attachments.
	 *
	 * @param attachment Body part {@link EmailAttachment}.
	 * @return {@link MimeBodyPart} which represents body part attachment.
	 * @throws MessagingException if there is a failure.
	 */
	protected MimeBodyPart createAttachmentBodyPart(final EmailAttachment<? extends DataSource> attachment) throws MessagingException {
		final MimeBodyPart part = new MimeBodyPart();

		final String attachmentName = attachment.getEncodedName();
		if (attachmentName != null) {
			part.setFileName(attachmentName);
		}

		part.setDataHandler(new DataHandler(attachment.getDataSource()));

		if (attachment.getContentId() != null) {
			part.setContentID(StringPool.LEFT_CHEV + attachment.getContentId() + StringPool.RIGHT_CHEV);
		}
		if (attachment.isInline()) {
			part.setDisposition(INLINE);
		}

		return part;
	}

	/**
	 * Filters out the {@link List} of embedded {@link EmailAttachment}s for given {@link EmailMessage}.
	 * This will remove the embedded attachments from the {@link List} and return them in a new {@link List}.
	 *
	 * @param attachments  {@link List} of attachments to search for in emailMessage.
	 * @param emailMessage {@link EmailMessage} to see if attachment is embedded into.
	 * @return {@link List} of embedded {@link EmailAttachment}s; otherwise, returns empty {@link List}.
	 */
	protected List<EmailAttachment<? extends DataSource>> filterEmbeddedAttachments(final List<EmailAttachment<? extends DataSource>> attachments, final EmailMessage emailMessage) {
		final List<EmailAttachment<? extends DataSource>> embeddedAttachments = new ArrayList<>();

		if (attachments == null || attachments.isEmpty() || emailMessage == null) {
			return embeddedAttachments;
		}

		final Iterator<EmailAttachment<? extends DataSource>> iterator = attachments.iterator();

		while (iterator.hasNext()) {
			final EmailAttachment<? extends DataSource> emailAttachment = iterator.next();

			if (emailAttachment.isEmbeddedInto(emailMessage)) {
				embeddedAttachments.add(emailAttachment);
				iterator.remove();
			}
		}

		return embeddedAttachments;
	}

	/**
	 * Adds {@link List} of {@link EmailAttachment}s to multipart.
	 *
	 * @param attachments {@link List} of {@link EmailAttachment}s to add to multipart. This can be {@code null}.
	 * @param multipart   {@link MimeMultipart} to set data into.
	 * @throws MessagingException if there is a failure.
	 */
	private void addAnyAttachments(final List<EmailAttachment<? extends DataSource>> attachments, final MimeMultipart multipart) throws MessagingException {
		for (final EmailAttachment<? extends DataSource> attachment : attachments) {
			final MimeBodyPart bodyPart = createAttachmentBodyPart(attachment);
			multipart.addBodyPart(bodyPart);
		}
	}

}


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

/**
 * A mutable int wrapper.
 */
public final class MutableInteger extends Number implements Comparable<MutableInteger>, Cloneable {

	public static MutableInteger of(final int value) {
		return new MutableInteger(value);
	}

	public MutableInteger() {
	}

	public MutableInteger(final int value) {
		this.value = value;
	}

	public MutableInteger(final String value) {
		this.value = Integer.parseInt(value);
	}

	public MutableInteger(final Number number) {
		this.value = number.intValue();
	}

	// ---------------------------------------------------------------- value

	/**
	 * The mutable value.
	 */
	public int value;

	/**
	 * Returns mutable value.
	 */
	public int get() {
		return value;
	}

	/**
	 * Sets mutable value.
	 */
	public void set(final int value) {
		this.value = value;
	}

	/**
	 * Sets mutable value from a Number.
	 */
	public void set(final Number value) {
		this.value = value.intValue();
	}

	// ---------------------------------------------------------------- object

	/**
	 * Stringify the value.
	 */
	@Override
	public String toString() {
		return Integer.toString(value);
	}

	/**
	 * Returns a hashcode for this value.
	 */
	@Override
	public int hashCode() {
		return value;
	}

	/**
	 * Compares this object to the specified object.
	 *
	 * @param obj the object to compare with.
	 * @return true if the objects are the same;
	 *         false otherwise.
	 */
	@Override
	public boolean equals(final Object obj) {
		if (obj != null) {
			if ( ((Integer)this.value).getClass() == obj.getClass() ) {
				return value == ((Integer) obj).intValue();
			}
			if (this.getClass() == obj.getClass()) {
				return value == ((MutableInteger) obj).value;
			}
		}
		return false;
	}

	// ---------------------------------------------------------------- number

	/**
	 * Returns the value as a int.
	 */
	@Override
	public int intValue() {
		return value;
	}

	/**
	 * Returns the value as a long.
	 */
	@Override
	public long longValue() {
		return value;
	}

	/**
	 * Returns the value as a float.
	 */
	@Override
	public float floatValue() {
		return value;
	}

	/**
	 * Returns the value as a double.
	 */
	@Override
	public double doubleValue() {
		return value;
	}

	// ---------------------------------------------------------------- compare

	/**
	 * Compares value of two same instances.
	 */
	@Override
	public int compareTo(final MutableInteger other) {
		return value < other.value ? -1 : (value == other.value ? 0 : 1);
	}

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

	/**
	 * Clones object.
	 */
	@Override
	public MutableInteger clone() {
		return new MutableInteger(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.io.upload;

import jodd.io.FileNameUtil;
import jodd.util.StringPool;

/**
 * Parses file upload header.
 */
public class FileUploadHeader {

	String dataHeader;
	String formFieldName;

	String formFileName;
	String path;
	String fileName;

	boolean isFile;
	String contentType;
	String mimeType;
	String mimeSubtype;
	String contentDisposition;

	FileUploadHeader(final String dataHeader) {
		this.dataHeader = dataHeader;
		isFile = dataHeader.indexOf("filename") > 0;
		formFieldName = getDataFieldValue(dataHeader, "name");
		if (isFile) {
			formFileName = getDataFieldValue(dataHeader, "filename");
			if (formFileName == null) {
				return;
			}
			if (formFileName.length() == 0) {
				path = StringPool.EMPTY;
				fileName = StringPool.EMPTY;
			}
			int ls = FileNameUtil.indexOfLastSeparator(formFileName);
			if (ls == -1) {
				path = StringPool.EMPTY;
				fileName = formFileName;
			} else {
				path = formFileName.substring(0, ls);
				fileName = formFileName.substring(ls + 1);
			}
			if (fileName.length() > 0) {
				this.contentType = getContentType(dataHeader);
				mimeType = getMimeType(contentType);
				mimeSubtype = getMimeSubtype(contentType);
				contentDisposition = getContentDisposition(dataHeader);
			}
		}
	}

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

	/**
	 * Gets value of data field or null if field not found.
	 */
	private String getDataFieldValue(final String dataHeader, final String fieldName) {
		String value = null;
		String token = String.valueOf((new StringBuffer(String.valueOf(fieldName))).append('=').append('"'));
		int pos = dataHeader.indexOf(token);
		if (pos > 0) {
			int start = pos + token.length();
			int end = dataHeader.indexOf('"', start);
			if ((start > 0) && (end > 0)) {
				value = dataHeader.substring(start, end);
			}
		}
		return value;
	}

	/**
	 * Strips content type information from requests data header.
	 * @param dataHeader data header string
	 * @return content type or an empty string if no content type defined
	 */
	private String getContentType(final String dataHeader) {
		String token = "Content-Type:";
		int start = dataHeader.indexOf(token);
		if (start == -1) {
			return StringPool.EMPTY;
		}
		start += token.length();
		return dataHeader.substring(start).trim();
	}

	private String getContentDisposition(final String dataHeader) {
		int start = dataHeader.indexOf(':') + 1;
		int end = dataHeader.indexOf(';');
		return dataHeader.substring(start, end);
	}

	private String getMimeType(final String ContentType) {
		int pos = ContentType.indexOf('/');
		if (pos == -1) {
			return ContentType;
		}
		return ContentType.substring(1, pos);
	}

	private String getMimeSubtype(final String ContentType) {
		int start = ContentType.indexOf('/');
		if (start == -1) {
			return ContentType;
		}
		start++;
		return ContentType.substring(start);
	}

	// ---------------------------------------------------------------- public interface

	/**
	 * Returns true if uploaded data are correctly marked as a file.
	 * This is true if header contains string 'filename'.
	 */
	public boolean isFile() {
		return isFile;
	}

	/**
	 * Returns form field name.
	 */
	public String getFormFieldName() {
		return formFieldName;
	}

	/**
	 * Returns complete file name as specified at client side.
	 */
	public String getFormFilename() {
		return formFileName;
	}

	/**
	 * Returns file name (base name and extension, without full path data).
	 */
	public String getFileName() {
		return fileName;
	}

	/**
	 * Returns uploaded content type. It is usually in the following form:

	 * mime_type/mime_subtype.
	 *
	 * @see #getMimeType()
	 * @see #getMimeSubtype()
	 */
	public String getContentType() {
		return contentType;
	}

	/**
	 * Returns file types MIME.
	 */
	public String getMimeType() {
		return mimeType;
	}

	/**
	 * Returns file sub type MIME.
	 */
	public String getMimeSubtype() {
		return mimeSubtype;
	}

	/**
	 * Returns content disposition. Usually it is 'form-data'.
	 */
	public String getContentDisposition() {
		return contentDisposition;
	}
}


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

package jodd.db;

import jodd.db.debug.LoggableCallableStatement;
import jodd.db.debug.LoggablePreparedStatement;
import jodd.log.Logger;
import jodd.log.LoggerFactory;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static jodd.db.DbQueryBase.State.CLOSED;
import static jodd.db.DbQueryBase.State.CREATED;
import static jodd.db.DbQueryBase.State.INITIALIZED;

/**
 * Support for {@link DbQuery} holds all configuration, initialization and the execution code.
 */
abstract class DbQueryBase<Q extends DbQueryBase> implements AutoCloseable {

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

	protected final DbOom dbOom;

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

	protected DbQueryBase(final DbOom dbOom) {
		this.dbOom = dbOom;
		this.forcePreparedStatement = dbOom.queryConfig().isForcePreparedStatement();
		this.type = dbOom.queryConfig().getType();
		this.concurrencyType = dbOom.queryConfig().getConcurrencyType();
		this.holdability = dbOom.queryConfig().getHoldability();
		this.debug = dbOom.queryConfig().isDebug();
		this.fetchSize = dbOom.queryConfig().getFetchSize();
		this.maxRows = dbOom.queryConfig().getMaxRows();
	}

	@SuppressWarnings("unchecked")
	protected Q _this() {
		return (Q) this;
	}

	// ---------------------------------------------------------------- query states

	/**
	 * Query states.
	 */
	public enum State {
		CREATED, INITIALIZED, CLOSED
	}
	protected State queryState = CREATED;

	/**
	 * Returns query state.
	 */
	public State getQueryState() {
		return queryState;
	}

	/**
	 * Checks if query is not closed and throws an exception if it is.
	 */
	protected void checkNotClosed() {
		if (queryState == CLOSED) {
			throw new DbSqlException(this, "Query is closed. Operation may be performed only on active queries.");
		}
	}

	/**
	 * Checks if query is created (and not yet initialized or closed) and throws an exception if it is not.
	 */
	protected void checkCreated() {
		if (queryState != CREATED) {
			final String message = (queryState == INITIALIZED ?
									"Query is already initialized." : "Query is closed.");
			throw new DbSqlException(this, message + " Operation may be performed only on created queries.");
		}
	}

	/**
	 * Checks if query is initialized and throws an exception if it is not.
	 */
	protected void checkInitialized() {
		if (queryState != INITIALIZED) {
			final String message = (queryState == CREATED ?
									"Query is created but not yet initialized." : "Query is closed.");
			throw new DbSqlException(this, message + " Operation may be performed only on initialized queries.");
		}
	}

	/**
	 * Returns true if query is closed.
	 */
	public boolean isClosed() {
		return queryState == CLOSED;
	}

	/**
	 * Returns true if query is active: created and possibly initialized.
	 * Opened query may be not initialized.
	 */
	public boolean isActive() {
		return queryState != CLOSED;
	}

	/**
	 * Returns true if query is initialized. Initialized query is the one that has
	 * created JDBC statements.
	 */
	public boolean isInitialized() {
		return queryState == INITIALIZED;
	}

	// ---------------------------------------------------------------- input

	protected Connection connection;
	protected DbSession session;
	protected String sqlString;

	/**
	 * Returns used {@link DbSession}.
	 */
	public DbSession getSession() {
		return session;
	}

	// ---------------------------------------------------------------- statement

	protected Statement statement;
	protected PreparedStatement preparedStatement;
	protected CallableStatement callableStatement;
	protected Set<ResultSet> resultSets;
	protected DbQueryParser query;

	/**
	 * Stores result set.
	 */
	protected void saveResultSet(final ResultSet rs) {
		if (resultSets == null) {
			resultSets = new HashSet<>();
		}
		resultSets.add(rs);
	}

	// ---------------------------------------------------------------- configuration

	/**
	 * If set to true all created statements will be prepared.
	 */
	protected boolean forcePreparedStatement;

	/**
	 * Forces creation of prepared statements.
	 */
	public Q forcePreparedStatement() {
		checkCreated();
		forcePreparedStatement = true;
		return _this();
	}

	// ---------------------------------------------------------------- initialization

	/**
	 * Initializes the query if not already initialized.
	 * Usually, user doesn't have to invoke it at all, since it will
	 * be called before all methods that deals directly with JDBC statement.
	 * Throws an exception if called on closed query.
	 */
	public final void init() {
		checkNotClosed();
		if (queryState == INITIALIZED) {
			return;
		}
		initializeJdbc();
		queryState = INITIALIZED;
		prepareQuery();
	}

	/**
	 * Initializes session. When not specified (i.e. is null),
	 * session is fetched from session provider.
	 */
	protected void initSession(final DbSession session) {
		if (session != null) {
			this.session = session;
			return;
		}

		final DbSessionProvider dbSessionProvider = dbOom.sessionProvider();

		this.session = dbSessionProvider.getDbSession();
	}

	/**
	 * Performs JDBC initialization of the query. Obtains connection, parses the SQL query string
	 * and creates statements. Initialization is performed only once, when switching to initialized state.
	 */
	@SuppressWarnings("MagicConstant")
	protected void initializeJdbc() {
		// connection
		if (connection == null) {
			initSession(session);

			connection = session.getConnection();
		}

		this.query = new DbQueryParser(sqlString);

		// callable statement

		if (query.callable) {
			try {
				if (debug) {
					if (holdability != QueryHoldability.DEFAULT) {
						callableStatement = new LoggableCallableStatement(
							connection.prepareCall(query.sql, type.value(), concurrencyType.value(), holdability.value()),
							query.sql
						);
					} else {
						callableStatement = new LoggableCallableStatement(
							connection.prepareCall(query.sql, type.value(), concurrencyType.value()),
							query.sql
						);
					}
				}
				else {
					if (holdability != QueryHoldability.DEFAULT) {
						callableStatement = connection.prepareCall(
							query.sql, type.value(), concurrencyType.value(), holdability.value());
					} else {
						callableStatement = connection.prepareCall(
							query.sql, type.value(), concurrencyType.value());
					}
				}
			}
			catch (SQLException sex) {
				throw new DbSqlException(this, "Error creating callable statement", sex);
			}

			preparedStatement = callableStatement;
			statement = callableStatement;

			return;
		}

		// prepared statement

		if (query.prepared || forcePreparedStatement) {
			try {
				if (debug) {
					if (generatedColumns != null) {
						if (generatedColumns.length == 0) {
							preparedStatement = new LoggablePreparedStatement(
								connection.prepareStatement(query.sql, Statement.RETURN_GENERATED_KEYS),
								query.sql);
						} else {
							preparedStatement = new LoggablePreparedStatement(
								connection.prepareStatement(query.sql, generatedColumns),
								query.sql);
						}
					} else {
						if (holdability != QueryHoldability.DEFAULT) {
							preparedStatement = new LoggablePreparedStatement(
								connection.prepareStatement(query.sql, type.value(), concurrencyType.value(), holdability.value()),
								query.sql);
						} else {
							preparedStatement = new LoggablePreparedStatement(
								connection.prepareStatement(query.sql, type.value(), concurrencyType.value()),
								query.sql);
						}
					}
				} else {
					if (generatedColumns != null) {
						if (generatedColumns.length == 0) {
							preparedStatement = connection.prepareStatement(query.sql, Statement.RETURN_GENERATED_KEYS);
						} else {
							preparedStatement = connection.prepareStatement(query.sql, generatedColumns);
						}
					} else {
						if (holdability != QueryHoldability.DEFAULT) {
							preparedStatement = connection.prepareStatement(
								query.sql, type.value(), concurrencyType.value(), holdability.value());
						} else {
							preparedStatement = connection.prepareStatement(
								query.sql, type.value(), concurrencyType.value());
						}
					}
				}
			}
			catch (SQLException sex) {
				throw new DbSqlException(this, "Error creating prepared statement", sex);
			}

			statement = preparedStatement;

			return;
		}

		// statement

		try {
			if (holdability != QueryHoldability.DEFAULT) {
				statement = connection.createStatement(type.value(), concurrencyType.value(), holdability.value());
			} else {
				statement = connection.createStatement(type.value(), concurrencyType.value());
			}
		} catch (SQLException sex) {
			throw new DbSqlException(this, "Error creating statement", sex);
		}
	}

	/**
	 * Prepares the query just after the initialization.
	 * Query is fully set and ready.
	 */
	protected void prepareQuery() {
		if (fetchSize != 0) {
			setFetchSize(fetchSize);
		}
		if (maxRows != 0) {
			setMaxRows(maxRows);
		}
	}

	// ---------------------------------------------------------------- close

	protected boolean autoClose;

	/**
	 * Defines that query should be automatically closed immediately after using.
	 * Should be called before actual statement execution.
	 */
	public Q autoClose() {
		autoClose = true;
		return _this();
	}

	/**
	 * Closes all result sets opened by this query. Query remains active.
	 * Returns SQLException (stacked with all exceptions)
	 * or null.
	 */
	private SQLException closeQueryResultSets() {
		SQLException sqlException = null;

		if (resultSets != null) {
			for (final ResultSet rs : resultSets) {
				try {
					rs.close();
				} catch (SQLException sex) {
					if (sqlException == null) {
						sqlException = sex;
					} else {
						sqlException.setNextException(sex);
					}
				} finally {
					totalOpenResultSetCount--;
				}
			}
			resultSets.clear();
			resultSets = null;
		}
		return sqlException;
	}

	/**
	 * Closes all result sets created by this query. Query remains active.
	 */
	public Q closeAllResultSets() {
		final SQLException sex = closeQueryResultSets();
		if (sex != null) {
			throw new DbSqlException("Close associated ResultSets error", sex);
		}
		return _this();
	}

	/**
	 * Closes all assigned result sets and then closes the query. Query becomes closed.
	 */
	protected SQLException closeQuery() {
		SQLException sqlException = closeQueryResultSets();
		if (statement != null) {
			try {
				statement.close();
			} catch (SQLException sex) {
				if (sqlException == null) {
					sqlException = sex;
				} else {
					sqlException.setNextException(sex);
				}
			}
			statement = null;
		}
		query = null;
		queryState = CLOSED;
		return sqlException;
	}

	/**
	 * Closes the query and all created results sets and detaches itself from the session.
	 */
	@Override
	@SuppressWarnings({"ClassReferencesSubclass"})
	public void close() {
		final SQLException sqlException = closeQuery();
		connection = null;
		if (this.session != null) {
			this.session.detachQuery(this);
		}
		if (sqlException != null) {
			throw new DbSqlException("Close query error", sqlException);
		}
	}

	/**
	 * Closes single result set that was created by this query. It is not necessary to close result sets
	 * explicitly, since {@link DbQueryBase#close()} method closes all created result sets.
	 * Query remains active.
	 */
	public void closeResultSet(final ResultSet rs) {
		if (rs == null) {
			return;
		}
		if (!resultSets.remove(rs)) {
			throw new DbSqlException(this, "ResultSet is not created by this query");
		}
		try {
			rs.close();
		} catch (SQLException sex) {
			throw new DbSqlException(this, "Close result set error", sex);
		} finally {
			totalOpenResultSetCount--;
		}
	}

	// ---------------------------------------------------------------- result set type

	protected QueryScrollType type;

	public QueryScrollType getType() {
		return type;
	}

	public Q setType(final QueryScrollType type) {
		checkCreated();
		this.type = type;
		return _this();
	}
	public Q typeForwardOnly() {
		setType(QueryScrollType.FORWARD_ONLY);
		return _this();
	}
	public Q typeScrollSensitive() {
		setType(QueryScrollType.SCROLL_SENSITIVE);
		return _this();
	}
	public Q typeScrollInsensitive() {
		setType(QueryScrollType.SCROLL_INSENSITIVE);
		return _this();
	}

	// ---------------------------------------------------------------- concurrency

	protected QueryConcurrencyType concurrencyType;

	public QueryConcurrencyType getConcurrencyType() {
		return concurrencyType;
	}

	public Q setConcurrencyType(final QueryConcurrencyType concurrencyType) {
		checkCreated();
		this.concurrencyType = concurrencyType;
		return _this();
	}
	public Q concurrentReadOnly() {
		setConcurrencyType(QueryConcurrencyType.READ_ONLY);
		return _this();
	}
	public Q concurrentUpdatable() {
		setConcurrencyType(QueryConcurrencyType.UPDATABLE);
		return _this();
	}

	// ---------------------------------------------------------------- holdability

	protected QueryHoldability holdability;

	public QueryHoldability getHoldability() {
		return holdability;
	}

	public Q setHoldability(final QueryHoldability holdability) {
		checkCreated();
		this.holdability = holdability;
		return _this();
	}

	public Q holdCursorsOverCommit() {
		setHoldability(QueryHoldability.HOLD_CURSORS_OVER_COMMIT);
		return _this();
	}

	public Q closeCursorsAtCommit() {
		setHoldability(QueryHoldability.CLOSE_CURSORS_AT_COMMIT);
		return _this();
	}

	// ---------------------------------------------------------------- debug mode

	protected boolean debug;

	public boolean isInDebugMode() {
		return debug;
	}

	public Q setDebug(final boolean debug) {
		checkCreated();
		this.debug = debug;
		return _this();
	}
	public Q setDebugMode() {
		setDebug(true);
		return _this();
	}

	// ---------------------------------------------------------------- generated keys

	protected String[] generatedColumns;

	/**
	 * Returns generated column names.
	 */
	public String[] getGeneratedColumnNames() {
		return generatedColumns;
	}

	/**
	 * Specifies columns which values will be generated by database.
	 */
	public Q setGeneratedColumns(final String... columns) {
		checkCreated();
		generatedColumns = columns;
		return _this();
	}

	/**
	 * Specifies that database will generate some columns values,
	 * usually the single id.
	 */
	public Q setGeneratedKey() {
		setGeneratedColumns();
		return _this();
	}

	/**
	 * Resets creating generated columns.
	 */
	public Q resetGeneratedColumns() {
		checkCreated();
		generatedColumns = null;
		return _this();
	}

	// ---------------------------------------------------------------- performance hints

	protected int fetchSize;

	/**
	 * Returns fetch size.
	 * @see #setFetchSize(int)
	 */
	public int getFetchSize() {
		return fetchSize;
	}

	/**
	 * Gives the JDBC driver a hint as to the number of rows that should be fetched from the database when
	 * more rows are needed. The number of rows specified affects only result sets created using this statement.
	 * If the value specified is zero, then the hint is ignored. The default value is zero.
	 * @see Statement#setFetchSize(int)
	 */
	public Q setFetchSize(final int rows) {
		checkNotClosed();
		this.fetchSize = rows;
		if (statement != null) {
			try {
				statement.setFetchSize(fetchSize);
			} catch (SQLException sex) {
				throw new DbSqlException(this, "Unable to set fetch size: " + fetchSize, sex);
			}
		}
		return _this();
	}

	protected int maxRows;

	/**
	 * Returns max rows.
	 * @see #setMaxRows(int)
	 */
	public int getMaxRows() {
		return maxRows;
	}

	/**
	 * Sets the limit for the maximum number of rows that any ResultSet object can contain to the given number.
	 * If the limit is exceeded, the excess rows are silently dropped. Zero means there is no limit.
	 * @see Statement#setMaxRows(int)
	 */
	public Q setMaxRows(final int maxRows) {
		checkNotClosed();
		this.maxRows = maxRows;
		if (statement != null) {
			try {
				statement.setMaxRows(maxRows);
			} catch (SQLException sex) {
				throw new DbSqlException(this, "Unable to set max rows: " + maxRows, sex);
			}
		}
		return _this();
	}

	// ---------------------------------------------------------------- execute
	protected long start;
	protected long elapsed = -1;

	/**
	 * Returns query execution elapsed time in milliseconds.
	 * Returns -1 if query is still not executed.
	 */
	public long getExecutionTime() {
		return elapsed;
	}

	/**
	 * Executes the query. If this method is invoked at least once, the query or
	 * all created ResultSets must be explicitly closed at the end of query usage.
	 * This can be done explicitly by calling {@link DbQueryBase#closeResultSet(ResultSet)}
	 * оr implicitly, during {@link DbQueryBase#close()}.
	 * @see Statement#execute(String)
	 */
	public ResultSet execute() {
		start = System.currentTimeMillis();

		init();
		ResultSet rs = null;
		if (log.isDebugEnabled()) {
			log.debug("Executing statement: " + getQueryString());
		}
		try {
			if (preparedStatement == null) {
				rs = statement.executeQuery(query.sql);
			} else {
				rs = preparedStatement.executeQuery();
			}
			rs.setFetchSize(fetchSize);
		} catch (SQLException sex) {
			DbUtil.close(rs);
			throw new DbSqlException(this, "Query execution failed", sex);
		}
		saveResultSet(rs);
		totalOpenResultSetCount++;

		elapsed = System.currentTimeMillis() - start;
		if (log.isDebugEnabled()) {
			log.debug("execution time: " + elapsed + "ms");
		}
		return rs;
	}

	public DbCallResult executeCall() {
		start = System.currentTimeMillis();

		init();
		if (log.isDebugEnabled()) {
			log.debug("Calling statement: " + getQueryString());
		}
		try {
			callableStatement.execute();
		} catch (SQLException sex) {
			DbUtil.close(callableStatement);
			throw new DbSqlException(this, "Query execution failed", sex);
		}

		elapsed = System.currentTimeMillis() - start;
		if (log.isDebugEnabled()) {
			log.debug("execution time: " + elapsed + "ms");
		}
		return new DbCallResult(query, callableStatement);
	}

	/**
	 * Executes UPDATE, INSERT or DELETE queries. Query is not closed afterwards
	 * unless {@link #autoClose() auto close mode} is set.
	 * @see Statement#executeUpdate(String)
	 */
	public int executeUpdate() {
		return executeUpdate(autoClose);
	}

	/**
	 * Executes UPDATE, INSERT or DELETE queries and optionally closes the query.
	 * @see Statement#executeUpdate(String)
	 */
	protected int executeUpdate(final boolean closeQuery) {
		start = System.currentTimeMillis();

		init();
		final int result;
		if (log.isDebugEnabled()) {
			log.debug("Executing update: " + getQueryString());
		}
		try {
			if (preparedStatement == null) {
				if (generatedColumns != null) {
					if (generatedColumns.length == 0) {
						result = statement.executeUpdate(query.sql, Statement.RETURN_GENERATED_KEYS);
					} else {
						result = statement.executeUpdate(query.sql, generatedColumns);
					}
				} else {
					result = statement.executeUpdate(query.sql);
				}
			} else {
				result = preparedStatement.executeUpdate();
			}
		} catch (SQLException sex) {
			throw new DbSqlException(this, "Query execution failed", sex);
		}
		if (closeQuery) {
			close();
		}

		elapsed = System.currentTimeMillis() - start;
		if (log.isDebugEnabled()) {
			log.debug("execution time: " + elapsed + "ms");
		}
		return result;
	}

	/**
	 * Special execute() for 'select count(*)' queries. Query is not closed after the execution
	 * unless {@link #autoClose() auto-close mode} is set.
	 * Doesn't check if query is really a count query, so it would work for any
	 * query that has number in the first column of result.
	 * If result set returns zero rows (very unlikely), returns -1.
	 */
	public long executeCount() {
		return executeCount(autoClose);
	}

	/**
	 * Executes count queries and optionally closes query afterwards.
	 */
	protected long executeCount(final boolean close) {
		start = System.currentTimeMillis();

		init();
		ResultSet rs = null;
		if (log.isDebugEnabled()) {
			log.debug("Executing prepared count: " + getQueryString());
		}
		try {
			if (preparedStatement == null) {
				rs = statement.executeQuery(query.sql);
			} else {
				rs = preparedStatement.executeQuery();
			}

			final long firstLong = DbUtil.getFirstLong(rs);

			elapsed = System.currentTimeMillis() - start;
			if (log.isDebugEnabled()) {
				log.debug("execution time: " + elapsed + "ms");
			}

			return firstLong;
		} catch (SQLException sex) {
			throw new DbSqlException(this, "Count query failed", sex);
		} finally {
			DbUtil.close(rs);
			if (close) {
				close();
			}
		}
	}

	// ---------------------------------------------------------------- result set mapper

	/**
	 * {@link #execute() Executes} the query, iterates result set and
	 * {@link QueryMapper maps} each row.
	 */
	public <T> List<T> list(final QueryMapper<T> queryMapper) {
		final ResultSet resultSet = execute();

		final List<T> list = new ArrayList<>();

		try {
			while (resultSet.next()) {
				final T t = queryMapper.process(resultSet);
				if (t == null) {
					break;
				}
				list.add(t);
			}
		} catch (SQLException sex) {
			throw new DbSqlException(sex);
		} finally {
			DbUtil.close(resultSet);
		}

		return list;
	}

	/**
	 * {@link #execute() Executes} the query and {@link QueryMapper maps} single result row.
	 */
	public <T> T find(final QueryMapper<T> queryMapper) {
		final ResultSet resultSet = execute();

		try {
			if (resultSet.next()) {
				return queryMapper.process(resultSet);
			}
		} catch (SQLException sex) {
			throw new DbSqlException(sex);
		} finally {
			DbUtil.close(resultSet);
		}
		return null;
	}

	/**
	 * {@link #execute() Executes} the query, iterates all rows and
	 * {@link QueryMapper maps} them.
	 */
	public <T> Set<T> listSet(final QueryMapper<T> queryMapper) {
		final ResultSet resultSet = execute();

		final Set<T> set = new HashSet<>();

		try {
			while (resultSet.next()) {
				final T t = queryMapper.process(resultSet);
				if (t == null) {
					break;
				}
				set.add(t);
			}
		} catch (SQLException sex) {
			throw new DbSqlException(sex);
		} finally {
			DbUtil.close(resultSet);
		}
		return set;
	}

	// ---------------------------------------------------------------- generated keys

	/**
	 * Returns generated columns.
	 */
	public ResultSet getGeneratedColumns() {
		checkInitialized();
		if (generatedColumns == null) {
			throw new DbSqlException(this, "No column is specified as auto-generated");
		}
		final ResultSet rs;
		try {
			rs = statement.getGeneratedKeys();
		} catch (SQLException sex) {
			throw new DbSqlException(this, "No generated keys", sex);
		}
		saveResultSet(rs);
		totalOpenResultSetCount++;
		return rs;
	}

	/**
	 * Returns generated key i.e. first generated column as long.
	 */
	public long getGeneratedKey() {
		checkInitialized();
		final ResultSet rs = getGeneratedColumns();
		try {
			return DbUtil.getFirstLong(rs);
		} catch (SQLException sex) {
			throw new DbSqlException(this, "No generated key as long", sex);
		} finally {
			DbUtil.close(rs);
			resultSets.remove(rs);
			totalOpenResultSetCount--;
		}
	}

	public Object getGeneratedKeyObject() {
		checkInitialized();
		final ResultSet rs = getGeneratedColumns();
		try {
			return DbUtil.getFirstObject(rs);
		} catch (SQLException sex) {
			throw new DbSqlException(this, "No generated key as long", sex);
		} finally {
			DbUtil.close(rs);
			resultSets.remove(rs);
			totalOpenResultSetCount--;
		}
	}

	// ---------------------------------------------------------------- query string

	/**
	 * Returns query SQL string. For prepared statements, returned sql string with quick-and-dirty replaced values.
	 */
	public String getQueryString() {
		if (debug) {
			if ((callableStatement != null)) {
				if (preparedStatement instanceof LoggableCallableStatement) {
					return ((LoggableCallableStatement) callableStatement).getQueryString();
				}
			}
			if (preparedStatement != null) {
				if (preparedStatement instanceof LoggablePreparedStatement) {
					return ((LoggablePreparedStatement) preparedStatement).getQueryString();
				}
			}
		}
		if (query != null) {
			return query.sql;
		}
		return sqlString;
	}

	/**
	 * @see #getQueryString()
	 */
	@Override
	public String toString() {
		return getQueryString();
	}

	// ---------------------------------------------------------------- statistics

	protected static int totalOpenResultSetCount;

	/**
	 * Returns total number of open result sets.
	 * @see #getOpenResultSetCount()
	 */
	public static int getTotalOpenResultSetCount() {
		return totalOpenResultSetCount;
	}

	/**
	 * Returns number of created result sets that are still not explicitly closed.
	 * @see #getTotalOpenResultSetCount()
	 */
	public int getOpenResultSetCount() {
		return resultSets == null ? 0 : resultSets.size();
	}
}


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

import jodd.core.JoddCore;
import jodd.util.StringUtil;

import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import java.io.UnsupportedEncodingException;

/**
 * Storage for personal name and email address.
 * Serves as an email address adapter between various formats.
 */
public class EmailAddress {

	public static final EmailAddress[] EMPTY_ARRAY = new EmailAddress[0];

	/**
	 * Email address.
	 */
	private final String email;

	/**
	 * Personal name.
	 */
	private final String personalName;

	/**
	 * Creates new address by specifying email and personal name.
	 *
	 * @param personalName personal name.
	 * @param email        email address.
	 */
	public EmailAddress(final String personalName, final String email) {
		this.email = email;
		this.personalName = personalName;
	}

	/**
	 * @see #EmailAddress(String, String)
	 */
	public static EmailAddress of(final String personalName, final String email) {
		return new EmailAddress(personalName, email);
	}

	/**
	 * Creates new address by specifying one of the following:
	 *

	 *
{@code "foo@bar.com" - only email address.}

	 *
{@code "Jenny Doe <foo@bar.com>" - first part of the string is personal name,
	 * and the other part is email, surrounded with < and >.}

	 *

	 *
	 * @param address {@link String} containing address to convert.
	 */
	public static EmailAddress of(String address) {
		address = address.trim();

		if (!StringUtil.endsWithChar(address, '>')) {
			return new EmailAddress(null, address);
		}

		final int ndx = address.lastIndexOf('<');
		if (ndx == -1) {
			return new EmailAddress(null, address);
		}

		String email = address.substring(ndx + 1, address.length() - 1);
		String personalName = address.substring(0, ndx).trim();
		return new EmailAddress(personalName, email);
	}

	/**
	 * Creates new email address from {@link InternetAddress}.
	 *
	 * @param internetAddress {@link InternetAddress} to convert
	 */
	public static EmailAddress of(final InternetAddress internetAddress) {
		return new EmailAddress(internetAddress.getPersonal(), internetAddress.getAddress());
	}

	/**
	 * Creates new email address from {@link InternetAddress}.
	 *
	 * @param address {@link Address} to convert.
	 */
	public static EmailAddress of(final Address address) {
		return of(address.toString());
	}

	// ---------------------------------------------------------------- getters

	/**
	 * Returns email address.
	 *
	 * @return email address.
	 */
	public String getEmail() {
		return email;
	}

	/**
	 * Returns personal name.
	 *
	 * @return personal name. Value may be {@code null}.
	 */
	public String getPersonalName() {
		return personalName;
	}

	/**
	 * Returns string representation of this.
	 *
	 * @return String representation of this.
	 */
	@Override
	public String toString() {
		if (this.personalName == null) {
			return this.email;
		}
		return this.personalName + " <" + this.email + '>';
	}

	// ---------------------------------------------------------------- convert

	/**
	 * Creates new {@link InternetAddress} from current data.
	 *
	 * @return {@link InternetAddress} from current data.
	 */
	public InternetAddress toInternetAddress() throws AddressException {
		try {
			return new InternetAddress(email, personalName, JoddCore.encoding);
		} catch (final UnsupportedEncodingException ueex) {
			throw new AddressException(ueex.toString());
		}
	}

	// ---------------------------------------------------------------- arrays

	/**
	 * Converts array of {@link Address} to {@link EmailAddress}.
	 *
	 * @param addresses array of {@link Address}es to convert.
	 * @return an array of {@link EmailAddress}.
	 */
	public static EmailAddress[] of(final Address... addresses) {
		if (addresses == null) {
			return EmailAddress.EMPTY_ARRAY;
		}
		if (addresses.length == 0) {
			return EmailAddress.EMPTY_ARRAY;
		}

		final EmailAddress[] res = new EmailAddress[addresses.length];

		for (int i = 0; i < addresses.length; i++) {
			res[i] = EmailAddress.of(addresses[i]);
		}

		return res;
	}

	/**
	 * Converts array of {@link String} to {@link EmailAddress}.
	 *
	 * @param addresses array of {@link String}s to convert.
	 * @return an array of {@link EmailAddress}.
	 */
	public static EmailAddress[] of(final String... addresses) {
		if (addresses == null) {
			return EmailAddress.EMPTY_ARRAY;
		}
		if (addresses.length == 0) {
			return EmailAddress.EMPTY_ARRAY;
		}

		final EmailAddress[] res = new EmailAddress[addresses.length];

		for (int i = 0; i < addresses.length; i++) {
			res[i] = EmailAddress.of(addresses[i]);
		}

		return res;
	}

	/**
	 * Convert from array of {@link EmailAddress} to array of {@link InternetAddress}.
	 *
	 * @param addresses {@link EmailMessage}
	 * @return array of {@link InternetAddress}. Returns empty array if addresses was {@code null}.
	 * @throws MessagingException if there are failures
	 */
	public static InternetAddress[] convert(final EmailAddress[] addresses) throws MessagingException {
		if (addresses == null) {
			return new InternetAddress[0];
		}

		final int numRecipients = addresses.length;
		final InternetAddress[] address = new InternetAddress[numRecipients];

		for (int i = 0; i < numRecipients; i++) {
			address[i] = addresses[i].toInternetAddress();
		}
		return address;
	}
}


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

import jodd.util.ClassUtil;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

/**
 * Factory for proxies, created using Java own library.
 */
public class AopProxy {

	/**
	 * Creates a proxy of given target and the aspect.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T proxyOf(final T target, final Class<? extends Aspect> aspectClass) {
		final Aspect aspect;

		try {
			aspect = ClassUtil.newInstance(aspectClass, target);
		}
		catch (Exception e) {
			throw new IllegalArgumentException("Can't create new instance of aspect class", e);
		}

		return (T) newProxyInstance(target.getClass().getClassLoader(), aspect, target.getClass().getInterfaces());
	}

	/**
	 * Creates a proxy from given {@link Aspect}.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T proxyOf(final Aspect aspect) {
		final Object target = aspect.getTarget();
		return (T) newProxyInstance(target.getClass().getClassLoader(), aspect, target.getClass().getInterfaces());
	}

	/**
	 * Simple wrapper for javas {@code newProxyInstance}.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T newProxyInstance(final ClassLoader classloader, final InvocationHandler invocationHandler, final Class<?>... interfaces) {
		if (interfaces.length == 0) {
			throw new IllegalArgumentException("No interfaces of target class found.");
		}
		return (T) Proxy.newProxyInstance(classloader, interfaces, invocationHandler);
	}

}