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

package jodd.util;

/**
 * Various character and character sequence utilities, including char[] - byte[] conversions.
 */
public class CharUtil {

	// ---------------------------------------------------------------- simple

	/**
	 * Converts (signed) byte to (unsigned) char.
	 */
	public static char toChar(final byte b) {
		return (char) (b & 0xFF);
	}

	/**
	 * Converts char array into byte array by stripping the high byte of each character.
	 */
	public static byte[] toSimpleByteArray(final char[] carr) {
		byte[] barr = new byte[carr.length];
		for (int i = 0; i < carr.length; i++) {
			barr[i] = (byte) carr[i];
		}
		return barr;
	}

	/**
	 * Converts char sequence into byte array.
	 * @see #toSimpleByteArray(char[])
	 */
	public static byte[] toSimpleByteArray(final CharSequence charSequence) {
		byte[] barr = new byte[charSequence.length()];
		for (int i = 0; i < barr.length; i++) {
			barr[i] = (byte) charSequence.charAt(i);
		}
		return barr;
	}

	/**
	 * Converts byte array to char array by simply extending bytes to chars.
	 */
	public static char[] toSimpleCharArray(final byte[] barr) {
		char[] carr = new char[barr.length];
		for (int i = 0; i < barr.length; i++) {
			carr[i] = (char) (barr[i] & 0xFF);
		}
		return carr;
	}

	// ---------------------------------------------------------------- ascii

	/**
	 * Returns ASCII value of a char. In case of overload, 0x3F is returned.
	 */
	public static int toAscii(final char c) {
		if (c <= 0xFF) {
			return c;
		} else {
			return 0x3F;
		}
	}

	/**
	 * Converts char array into {@link #toAscii(char) ASCII} array.
	 */
	public static byte[] toAsciiByteArray(final char[] carr) {
		byte[] barr = new byte[carr.length];
		for (int i = 0; i < carr.length; i++) {
			barr[i] = (byte) ((int) (carr[i] <= 0xFF ? carr[i] : 0x3F));
		}
		return barr;
	}

	/**
	 * Converts char sequence into ASCII byte array.
	 */
	public static byte[] toAsciiByteArray(final CharSequence charSequence) {
		byte[] barr = new byte[charSequence.length()];
		for (int i = 0; i < barr.length; i++) {
			char c = charSequence.charAt(i);
			barr[i] = (byte) ((int) (c <= 0xFF ? c : 0x3F));
		}
		return barr;
	}

	// ---------------------------------------------------------------- raw arrays

	/**
	 * Converts char array into byte array by replacing each character with two bytes.
	 */
	public static byte[] toRawByteArray(final char[] carr) {
		byte[] barr = new byte[carr.length << 1];
		for (int i = 0, bpos = 0; i < carr.length; i++) {
			char c = carr[i];
			barr[bpos++] = (byte) ((c & 0xFF00) >> 8);
			barr[bpos++] = (byte) (c & 0x00FF);
		}
		return barr;
	}

	public static char[] toRawCharArray(final byte[] barr) {
		int carrLen = barr.length >> 1;
		if (carrLen << 1 < barr.length) {
			carrLen++;
		}
		char[] carr = new char[carrLen];
		int i = 0, j = 0;
		while (i < barr.length) {
			char c = (char) (barr[i] << 8);
			i++;

			if (i != barr.length) {
				c += barr[i] & 0xFF;
				i++;
			}
			carr[j++] = c;
		}
		return carr;
	}

	// ---------------------------------------------------------------- encoding

	/**
	 * Converts char array to byte array using default Jodd encoding.
	 */
	public static byte[] toByteArray(final char[] carr) {
		return StringUtil.getBytes(new String(carr));
	}

	/**
	 * Converts char array to byte array using provided encoding.
	 */
	public static byte[] toByteArray(final char[] carr, final String charset) {
		return StringUtil.getBytes(new String(carr), charset);
	}

	/**
	 * Converts byte array of default Jodd encoding to char array.
	 */
	public static char[] toCharArray(final byte[] barr) {
		return StringUtil.newString(barr).toCharArray();
	}

	/**
	 * Converts byte array of specific encoding to char array.
	 */
	public static char[] toCharArray(final byte[] barr, final String charset) {
		return StringUtil.newString(barr, charset).toCharArray();
	}

	// ---------------------------------------------------------------- find

	/**
	 * Match if one character equals to any of the given character.
	 *
	 * @return true if characters match any character from given array,
	 *         otherwise false
	 */
	public static boolean equalsOne(final char c, final char[] match) {
		for (char aMatch : match) {
			if (c == aMatch) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Finds index of the first character in given array the matches any from the
	 * given set of characters.
	 *
	 * @return index of matched character or -1
	 */
	public static int findFirstEqual(final char[] source, final int index, final char[] match) {
		for (int i = index; i < source.length; i++) {
			if (equalsOne(source[i], match)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Finds index of the first character in given array the matches any from the
	 * given set of characters.
	 *
	 * @return index of matched character or -1
	 */
	public static int findFirstEqual(final char[] source, final int index, final char match) {
		for (int i = index; i < source.length; i++) {
			if (source[i] == match) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Finds index of the first character in given array the differs from the
	 * given set of characters.
	 *
	 * @return index of matched character or -1
	 */
	public static int findFirstDiff(final char[] source, final int index, final char[] match) {
		for (int i = index; i < source.length; i++) {
			if (!equalsOne(source[i], match)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Finds index of the first character in given array the differs from the
	 * given set of characters.
	 *
	 * @return index of matched character or -1
	 */
	public static int findFirstDiff(final char[] source, final int index, final char match) {
		for (int i = index; i < source.length; i++) {
			if (source[i] != match) {
				return i;
			}
		}
		return -1;
	}

	// ---------------------------------------------------------------- is

	/**
	 * Returns true if character is a white space ({@code <= ' '}).
	 * White space definition is taken from String class (see: trim()).
	 * This method has different results then Character#isWhitespace."
	 */
	public static boolean isWhitespace(final char c) {
		return c <= ' ';
	}

	/**
	 * Returns true if specified character is lowercase ASCII.
	 * If user uses only ASCIIs, it is much much faster.
	 */
	public static boolean isLowercaseAlpha(final char c) {
		return (c >= 'a') && (c <= 'z');
	}

	/**
	 * Returns true if specified character is uppercase ASCII.
	 * If user uses only ASCIIs, it is much much faster.
	 */
	public static boolean isUppercaseAlpha(final char c) {
		return (c >= 'A') && (c <= 'Z');
	}

	public static boolean isAlphaOrDigit(final char c) {
		return isDigit(c) || isAlpha(c);
	}

	public static boolean isWordChar(final char c) {
		return isDigit(c) || isAlpha(c) || (c == '_');
	}

	public static boolean isPropertyNameChar(final char c) {
		return isDigit(c) || isAlpha(c) || (c == '_') || (c == '.') || (c == '[') || (c == ']');
	}

	// ---------------------------------------------------------------- RFC

	/**
	 * Indicates whether the given character is in the {@code ALPHA} set.
	 *
	 * @see RFC 3986, appendix A
	 */
	public static boolean isAlpha(final char c) {
		return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
	}

	/**
	 * Indicates whether the given character is in the {@code DIGIT} set.
	 *
	 * @see RFC 3986, appendix A
	 */
	public static boolean isDigit(final char c) {
		return c >= '0' && c <= '9';
	}

	/**
	 * Indicates whether the given character is the hexadecimal digit.
	 */
	public static boolean isHexDigit(final char c) {
		return (c >= '0' && c <= '9') || ((c >= 'a') && (c <= 'f')) || ((c >= 'A') && (c <= 'F'));
	}

	/**
	 * Indicates whether the given character is in the gen-delims set.
	 *
	 * @see RFC 3986, appendix A
	 */
	public static boolean isGenericDelimiter(final int c) {
		switch (c) {
			case ':':
			case '/':
			case '?':
			case '#':
			case '[':
			case ']':
			case '@':
				return true;
			default:
				return false;
		}
	}

	/**
	 * Indicates whether the given character is in the sub-delims set.
	 *
	 * @see RFC 3986, appendix A
	 */
	public static boolean isSubDelimiter(final int c) {
		switch (c) {
			case '!':
			case '$':
			case '&':
			case '\'':
			case '(':
			case ')':
			case '*':
			case '+':
			case ',':
			case ';':
			case '=':
				return true;
			default:
				return false;
		}
	}

	/**
	 * Indicates whether the given character is in the reserved set.
	 *
	 * @see RFC 3986, appendix A
	 */
	public static boolean isReserved(final char c) {
		return isGenericDelimiter(c) || isSubDelimiter(c);
	}

	/**
	 * Indicates whether the given character is in the unreserved set.
	 *
	 * @see RFC 3986, appendix A
	 */
	public static boolean isUnreserved(final char c) {
		return isAlpha(c) || isDigit(c) || c == '-' || c == '.' || c == '_' || c == '~';
	}

	/**
	 * Indicates whether the given character is in the pchar set.
	 *
	 * @see RFC 3986, appendix A
	 */
	public static boolean isPchar(final char c) {
		return isUnreserved(c) || isSubDelimiter(c) || c == ':' || c == '@';
	}

	// ---------------------------------------------------------------- conversions

	/**
	 * Uppers lowercase ASCII char.
	 */
	public static char toUpperAscii(char c) {
		if (isLowercaseAlpha(c)) {
			c -= (char) 0x20;
		}
		return c;
	}

	/**
	 * Lowers uppercase ASCII char.
	 */
	public static char toLowerAscii(char c) {
		if (isUppercaseAlpha(c)) {
			c += (char) 0x20;
		}
		return c;
	}

	/**
	 * Converts hex char to int value.
	 */
	public static int hex2int(final char c) {
		switch (c) {
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
				return c - '0';
			case 'A':
			case 'B':
			case 'C':
			case 'D':
			case 'E':
			case 'F':
				return c - 55;
			case 'a':
			case 'b':
			case 'c':
			case 'd':
			case 'e':
			case 'f':
				return c - 87;
			default:
				throw new IllegalArgumentException("Not a hex: " + c);
		}
	}

	/**
	 * Converts integer digit to heck char.
	 */
	public static char int2hex(final int i) {
		return HEX_CHARS[i];
	}

	public static final char[] HEX_CHARS = new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

}


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

package jodd.lagarto.dom;

import jodd.lagarto.TagWriterUtil;
import jodd.net.HtmlEncoder;

import java.io.IOException;

/**
 * {@link jodd.lagarto.dom.NodeVisitor} that renders DOM tree to string.
 */
public class LagartoHtmlRendererNodeVisitor implements NodeVisitor {

	protected final Appendable appendable;

	public LagartoHtmlRendererNodeVisitor(final Appendable appendable) {
		this.appendable = appendable;
	}

	@Override
	public void cdata(final CData cdata) {
		String nodeValue = cdata.getNodeValue();
		try {
			TagWriterUtil.writeCData(appendable, nodeValue);
		} catch (IOException ioex) {
			throw new LagartoDOMException(ioex);
		}
	}

	@Override
	public void comment(final Comment comment) {
		String nodeValue = comment.getNodeValue();

		try {
			TagWriterUtil.writeComment(appendable, nodeValue);
		} catch (IOException ioex) {
			throw new LagartoDOMException(ioex);
		}
	}

	@Override
	public void document(final Document document) {
		document.visitChildren(this);
	}

	@Override
	public void documentType(final DocumentType documentType) {
		try {
			TagWriterUtil.writeDoctype(appendable,
					documentType.nodeValue,
					documentType.publicId,
					documentType.systemId);
		} catch (IOException ioex) {
			throw new LagartoDOMException(ioex);
		}
	}

	@Override
	public void text(final Text text) {
		String nodeValue = text.getTextValue();

		try {
			appendable.append(nodeValue);
		} catch (IOException ioex) {
			throw new LagartoDOMException(ioex);
		}
	}

	@Override
	public void xmlDeclaration(final XmlDeclaration xmlDeclaration) {
		try {
			TagWriterUtil.writeXml(appendable,
					xmlDeclaration.getVersion(), xmlDeclaration.getEncoding(), xmlDeclaration.getStandalone());
		} catch (IOException ioex) {
			throw new LagartoDOMException(ioex);
		}
	}

	// ---------------------------------------------------------------- element

	/**
	 * Enumeration of case options for output name.
	 */
	public enum Case {
		/**
		 * Default case, depends on parser case sensitivity.
		 */
		DEFAULT,
		/**
		 * Raw name, no modifications.
		 */
		RAW,
		/**
		 * Lowercase name.
		 */
		LOWERCASE,
		/**
		 * Uppercase name.
		 */
		UPPERCASE,
	}

	/**
	 * Letter case of tag names.
	 */
	protected Case tagCase = Case.DEFAULT;

	/**
	 * Letter case of attributes names.
	 */
	protected Case attributeCase = Case.DEFAULT;

	/**
	 * Sets {@link Case case} of tag names.
	 */
	public void setTagCase(final Case tagCase) {
		this.tagCase = tagCase;
	}

	public Case getTagCase() {
		return tagCase;
	}

	/**
	 * Sets {@link Case case} of attribute names.
	 */
	public void setAttributeCase(final Case attributeCase) {
		this.attributeCase = attributeCase;
	}

	public Case getAttributeCase() {
		return attributeCase;
	}

	/**
	 * Resets all cases to default.
	 */
	public void reset() {
		tagCase = Case.DEFAULT;
		attributeCase = Case.DEFAULT;
	}

	/**
	 * Renders node name.
	 */
	protected String resolveNodeName(final Node node) {
		switch (tagCase) {
			case DEFAULT: return node.getNodeName();
			case RAW: return node.getNodeRawName();
			case LOWERCASE: return node.getNodeRawName().toLowerCase();
			case UPPERCASE: return node.getNodeRawName().toUpperCase();
		}
		return null;
	}

	/**
	 * Renders attribute name.
	 */
	protected String resolveAttributeName(final Node node, final Attribute attribute) {
		switch (attributeCase) {
			case DEFAULT: return attribute.getName();
			case RAW: return attribute.getRawName();
			case LOWERCASE: return attribute.getRawName().toLowerCase();
			case UPPERCASE: return attribute.getRawName().toUpperCase();
		}
		return null;
	}

	/**
	 * Renders attribute.
	 */
	protected void renderAttribute(final Node node, final Attribute attribute, final Appendable appendable) throws IOException {
		String name = resolveAttributeName(node, attribute);
		String value = attribute.getValue();

		appendable.append(name);
		if (value != null) {
			appendable.append('=');
			appendable.append('\"');
			appendable.append(HtmlEncoder.attributeDoubleQuoted(value));
			appendable.append('\"');
		}
	}

	@Override
	public void element(final Element element) {
		try {
			_element(element);
		} catch (IOException ioex) {
			throw new LagartoDOMException(ioex);
		}
	}

	protected void _element(final Element element) throws IOException {
		String nodeName = resolveNodeName(element);

		appendable.append('<');
		appendable.append(nodeName);

		int attrCount = element.getAttributesCount();

		if (attrCount != 0) {
			for (int i = 0; i < attrCount; i++) {
				Attribute attr = element.getAttribute(i);
				appendable.append(' ');
				renderAttribute(element, attr, appendable);
			}
		}

		int childCount = element.getChildNodesCount();

		if (element.selfClosed && childCount == 0) {
			appendable.append("/>");
			return;
		}

		appendable.append('>');

		if (element.voidElement) {
			return;
		}

		if (childCount != 0) {
			elementBody(element);
		}

		appendable.append("</");
		appendable.append(nodeName);
		appendable.append('>');
	}

	protected void elementBody(final Element element) throws IOException {
		int childCount = element.getChildNodesCount();

		if (element.isRawTag()) {
			for (int i = 0; i < childCount; i++) {
				Node childNode = element.getChild(i);

				if (childNode.getNodeType() == Node.NodeType.TEXT) {
					appendable.append(childNode.getNodeValue());
				} else {
					childNode.visit(this);
				}
			}
		} else {
			element.visitChildren(this);
		}
	}

}


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

import jodd.core.JoddCore;
import jodd.exception.UncheckedException;
import jodd.io.FastCharArrayWriter;
import jodd.io.FileNameUtil;
import jodd.io.FileUtil;
import jodd.io.StreamUtil;
import jodd.io.findfile.ClassScanner;
import jodd.util.StringPool;
import jodd.util.StringUtil;
import jodd.util.Wildcard;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 * Super properties: fast, configurable, supports (ini) sections, profiles.
 *

 * Basic parsing rules:
 *

 *
 By default, props files are UTF8 encoded.
 *
 Leading and trailing spaces will be trimmed from section names and property names.
 *
 Leading and/or trailing spaces may be trimmed from property values.
 *
 You can use either equal sign (=) or colon (:) to assign property values
 *
 Comments begin with either a semicolon (;), or a sharp sign (#) and extend to the end of line. It doesn't have to be the first character.
 *
 A backslash (\) escapes the next character (e.g., \# is a literal #, \\ is a literal \).
 *
 If the last character of a line is backslash (\), the value is continued on the next line with new line character included.
 *
 \\uXXXX is encoded as character
 *
 \t, \r and \f are encoded as characters
 *

 *

 * Sections rules:
 *

 *
 Section names are enclosed between [ and ].
 *
 Properties following a section header belong to that section. Section name is added as a prefix to section properties.
 *
 Section ends with empty section definition [] or with new section start
 *

 *

 * Profiles rules:
 *

 *
 Profile names are enclosed between < and > in property key.
 *
 Each property key may contain zero, one or more profile definitions.
 *

 *

 * Macro rules:
 *

 *
 Profile values may contain references to other properties using ${ and }
 *
 Inner references are supported
 *
 References are resolved first in the profile context and then in the base props context.
 *

 */
public class Props implements Cloneable {

	private static final String DEFAULT_PROFILES_PROP = "@profiles";

	protected final PropsParser parser;

	protected final PropsData data;

	protected String activeProfilesProp = DEFAULT_PROFILES_PROP;

	protected String[] activeProfiles;

	protected volatile boolean initialized;

	/**
	 * Statis ctor.
	 */
	public static Props create() {
		return new Props();
	}

	/**
	 * Creates new props.
	 */
	public Props() {
		this(new PropsParser());
	}

	protected Props(final PropsParser parser) {
		this.parser = parser;
		this.data = parser.getPropsData();
	}

	/**
	 * Clones props by creating new instance and copying current configuration.
	 */
	@Override
	protected Props clone() {
		final PropsParser parser = this.parser.clone();
		final Props p = new Props(parser);

		p.activeProfilesProp = activeProfilesProp;
		return p;
	}

	/**
	 * Returns active profiles or null if none defined.
	 */
	public String[] getActiveProfiles() {
		initialize();
		return activeProfiles;
	}

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

	/**
	 * Sets new active profiles and overrides existing ones.
	 * By setting null, no active profile will be set.
	 *

	 * Note that if some props are loaded after
	 * this method call, they might override active profiles
	 * by using special property for active profiles (@profiles).
	 */
	public Props setActiveProfiles(final String... activeProfiles) {
		initialized = false;
		this.activeProfiles = activeProfiles;
		return this;
	}

	/**
	 * Specifies the new line string when EOL is escaped.
	 * Default value is an empty string.
	 */
	public Props setEscapeNewLineValue(final String escapeNewLineValue) {
		parser.escapeNewLineValue = escapeNewLineValue;
		return this;
	}

	/**
	 * Specifies should the values be trimmed from the left.
	 * Default is true.
	 */
	public Props setValueTrimLeft(final boolean valueTrimLeft) {
		parser.valueTrimLeft = valueTrimLeft;
		return this;
	}

	/**
	 * Specifies should the values be trimmed from the right.
	 * Default is true.
	 */
	public Props setValueTrimRight(final boolean valueTrimRight) {
		parser.valueTrimRight = valueTrimRight;
		return this;
	}

	/**
	 * Defines if the prefix whitespaces should be ignored when value is split into the lines.
	 */
	public Props setIgnorePrefixWhitespacesOnNewLine(final boolean ignorePrefixWhitespacesOnNewLine) {
		parser.ignorePrefixWhitespacesOnNewLine = ignorePrefixWhitespacesOnNewLine;
		return this;
	}

	/**
	 * Skips empty properties as they don't exist.
	 */
	public Props setSkipEmptyProps(final boolean skipEmptyProps) {
		parser.skipEmptyProps = skipEmptyProps;
		data.skipEmptyProps = skipEmptyProps;
		return this;
	}

	/**
	 * Appends duplicate props.
	 */
	public Props setAppendDuplicateProps(final boolean appendDuplicateProps) {
		data.appendDuplicateProps = appendDuplicateProps;
		return this;
	}

	/**
	 * Ignore missing macros by replacing them with an empty string.
	 */
	public Props setIgnoreMissingMacros(final boolean ignoreMissingMacros) {
		data.ignoreMissingMacros = ignoreMissingMacros;
		return this;
	}

	/**
	 * Enables multiline values.
	 */
	public Props setMultilineValues(final boolean multilineValues) {
		parser.multilineValues = multilineValues;
		return this;
	}

	/**
	 * Parses input string and loads provided properties map.
	 */
	protected synchronized void parse(final String data) {
		initialized = false;
		parser.parse(data);
	}

	// ---------------------------------------------------------------- load

	/**
	 * Loads props from the string.
	 */
	public Props load(final String data) {
		parse(data);
		return this;
	}

	/**
	 * Loads props from the file. Assumes UTF8 encoding unless
	 * the file ends with '.properties', than it uses ISO 8859-1.
	 */
	public Props load(final File file) throws IOException {
		final String extension = FileNameUtil.getExtension(file.getAbsolutePath());
		final String data;
		if (extension.equalsIgnoreCase("properties")) {
			data = FileUtil.readString(file, StringPool.ISO_8859_1);
		} else {
			data = FileUtil.readString(file);
		}
		parse(data);
		return this;
	}

	/**
	 * Loads properties from the file in provided encoding.
	 */
	public Props load(final File file, final String encoding) throws IOException {
		parse(FileUtil.readString(file, encoding));
		return this;
	}

	/**
	 * Loads properties from input stream. Stream is not closed at the end.
	 */
	public Props load(final InputStream in) throws IOException {
		final Writer out = new FastCharArrayWriter();
		StreamUtil.copy(in, out);
		parse(out.toString());
		return this;
	}

	/**
	 * Loads properties from input stream and provided encoding.
	 * Stream is not closed at the end.
	 */
	public Props load(final InputStream in, final String encoding) throws IOException {
		final Writer out = new FastCharArrayWriter();
		StreamUtil.copy(in, out, encoding);
		parse(out.toString());
		return this;
	}

	/**
	 * Loads base properties from the provided java properties.
	 * Null values are ignored.
	 */
	public Props load(final Map<?, ?> p) {
		for (final Map.Entry<?, ?> entry : p.entrySet()) {
			final String name = entry.getKey().toString();
			final Object value = entry.getValue();
			if (value == null) {
				continue;
			}
			data.putBaseProperty(name, value.toString(), false);
		}
		return this;
	}

	/**
	 * Loads base properties from java Map using provided prefix.
	 * Null values are ignored.
	 */
	@SuppressWarnings("unchecked")
	public Props load(final Map<?, ?> map, final String prefix) {
		String realPrefix = prefix;
		realPrefix += '.';
		for (final Map.Entry entry : map.entrySet()) {
			final String name = entry.getKey().toString();
			final Object value = entry.getValue();
			if (value == null) {
				continue;
			}
			data.putBaseProperty(realPrefix + name, value.toString(), false);
		}
		return this;
	}

	/**
	 * Loads system properties with given prefix.
	 * If prefix is null it will not be ignored.
	 */
	public Props loadSystemProperties(final String prefix) {
		final Properties environmentProperties = System.getProperties();
		load(environmentProperties, prefix);
		return this;
	}

	/**
	 * Loads environment properties with given prefix.
	 * If prefix is null it will not be used.
	 */
	public Props loadEnvironment(final String prefix) {
		final Map<String, String> environmentMap = System.getenv();
		load(environmentMap, prefix);
		return this;
	}

	/**
	 * Loads props and properties from the classpath.
	 */
	public Props loadFromClasspath(final String... patterns) {
		ClassScanner.create()
			.registerEntryConsumer(entryData -> {
				String usedEncoding = JoddCore.encoding;
				if (StringUtil.endsWithIgnoreCase(entryData.name(), ".properties")) {
					usedEncoding = StringPool.ISO_8859_1;
				}

				final String encoding = usedEncoding;
				UncheckedException.runAndWrapException(() -> load(entryData.openInputStream(), encoding));
			})
			.includeResources(true)
			.ignoreException(true)
			.excludeCommonJars()
			.excludeAllEntries(true)
			.includeEntries(patterns)
			.scanDefaultClasspath()
			.start();
		return this;
	}

	// ---------------------------------------------------------------- props

	/**
	 * Counts the total number of properties, including all profiles.
	 * This operation performs calculation each time and it might be
	 * more time consuming then expected.
	 */
	public int countTotalProperties() {
		return data.countBaseProperties() + data.countProfileProperties();
	}

	/**
	 * Returns string value of base property.
	 * Returns null if property doesn't exist.
	 */
	@SuppressWarnings({"NullArgumentToVariableArgMethod"})
	public String getBaseValue(final String key) {
		return getValue(key, StringPool.EMPTY_ARRAY);
	}

	/**
	 * Returns value of property, using active profiles, or {@code null} if property not found.
	 */
	public String getValue(final String key) {
		initialize();
		return data.lookupValue(key, activeProfiles);
	}

	/**
	 * Returns value of property, using active profiles or default value if not found.
	 */
	public String getValueOrDefault(final String key, final String defaultValue) {
		initialize();
		final String value = data.lookupValue(key, activeProfiles);
		if (value == null) {
			return defaultValue;
		}
		return value;
	}

	/**
	 * Returns integer value of given property or {@code null} if property not found.
	 */
	public Integer getIntegerValue(final String key) {
		final String value = getValue(key);
		if (value == null) {
			return null;
		}
		return Integer.valueOf(value);
	}

	/**
	 * Returns integer value or default one if property not defined.
	 */
	public Integer getIntegerValue(final String key, final Integer defaultValue) {
		final String value = getValue(key);
		if (value == null) {
			return defaultValue;
		}
		return Integer.valueOf(value);
	}

	/**
	 * Returns long value of given property or {@code null} if property not found.
	 */
	public Long getLongValue(final String key) {
		final String value = getValue(key);
		if (value == null) {
			return null;
		}
		return Long.valueOf(value);
	}

	/**
	 * Returns long value or default one if property not defined.
	 */
	public Long getLongValue(final String key, final Long defaultValue) {
		final String value = getValue(key);
		if (value == null) {
			return defaultValue;
		}
		return Long.valueOf(value);
	}

	/**
	 * Returns double value of given property or {@code null} if property not found.
	 */
	public Double getDoubleValue(final String key) {
		final String value = getValue(key);
		if (value == null) {
			return null;
		}
		return Double.valueOf(value);
	}

	/**
	 * Returns double value or default one if property not defined.
	 */
	public Double getDoubleValue(final String key, final Double defaultValue) {
		final String value = getValue(key);
		if (value == null) {
			return defaultValue;
		}
		return Double.valueOf(value);
	}

	/**
	 * Returns boolean value of given property or {@code null} if property not found.
	 */
	public Boolean getBooleanValue(final String key) {
		final String value = getValue(key);
		if (value == null) {
			return null;
		}
		return Boolean.valueOf(value);
	}

	/**
	 * Returns boolean value or default one if property not defined.
	 */
	public Boolean getBooleanValue(final String key, final Boolean defaultValue) {
		final String value = getValue(key);
		if (value == null) {
			return defaultValue;
		}
		return Boolean.valueOf(value);
	}

	/**
	 * Returns string value of given profiles. If key is not
	 * found under listed profiles, base properties will be searched.
	 * Returns null if property doesn't exist.
	 */
	public String getValue(final String key, final String... profiles) {
		initialize();
		return data.lookupValue(key, profiles);
	}

	public Integer getIntegerValue(final String key, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return null;
		}
		return Integer.valueOf(value);
	}
	public Integer getIntegerValue(final String key, final Integer defaultValue, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return defaultValue;
		}
		return Integer.valueOf(value);
	}
	public Long getLongValue(final String key, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return null;
		}
		return Long.valueOf(value);
	}
	public Long getLongValue(final String key, final Long defaultValue, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return defaultValue;
		}
		return Long.valueOf(value);
	}
	public Double getDoubleValue(final String key, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return null;
		}
		return Double.valueOf(value);
	}
	public Double getDoubleValue(final String key, final Double defaultValue, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return defaultValue;
		}
		return Double.valueOf(value);
	}
	public Boolean getBooleanValue(final String key, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return null;
		}
		return Boolean.valueOf(value);
	}
	public Boolean getBooleanValue(final String key, final Boolean defaultValue, final String... profiles) {
		final String value = getValue(key, profiles);
		if (value == null) {
			return defaultValue;
		}
		return Boolean.valueOf(value);
	}

	/**
	 * Sets default value.
	 */
	public void setValue(final String key, final String value) {
		setValue(key, value, null);
	}

	/**
	 * Sets value on some profile.
	 */
	public void setValue(final String key, final String value, final String profile) {
		if (profile == null) {
			data.putBaseProperty(key, value, false);
		} else {
			data.putProfileProperty(key, value, profile, false);
		}
		initialized = false;
	}

	// ---------------------------------------------------------------- extract

	/**
	 * Extracts props belonging to active profiles.
	 */
	public void extractProps(final Map target) {
		initialize();
		data.extract(target, activeProfiles, null, null);
	}

	/**
	 * Extract props of given profiles.
	 */
	public void extractProps(final Map target, final String... profiles) {
		initialize();
		data.extract(target, profiles, null, null);
	}

	/**
	 * Extracts subset of properties that matches given wildcards.
	 */
	public void extractSubProps(final Map target, final String... wildcardPatterns) {
		initialize();
		data.extract(target, activeProfiles, wildcardPatterns, null);
	}

	/**
	 * Extracts subset of properties that matches given wildcards.
	 */
	public void extractSubProps(final Map target, final String[] profiles, final String[] wildcardPatterns) {
		initialize();
		data.extract(target, profiles, wildcardPatterns, null);
	}

	// ---------------------------------------------------------------- childMap

	/**
	 * Returns inner map from the props with given prefix. Keys in returned map
	 * will not have the prefix.
	 */
	@SuppressWarnings("unchecked")
	public Map<String, Object> innerMap(final String prefix) {
		initialize();
		return data.extract(null, activeProfiles, null, prefix);
	}

	/**
	 * Adds child map to the props on given prefix.
	 */
	public void addInnerMap(final String prefix, final Map<?, ?> map) {
		addInnerMap(prefix, map, null);
	}

	/**
	 * Adds child map to the props on given prefix.
	 */
	public void addInnerMap(String prefix, final Map<?, ?> map, final String profile) {
		if (!StringUtil.endsWithChar(prefix, '.')) {
			prefix += StringPool.DOT;
		}

		for (Map.Entry<?, ?> entry : map.entrySet()) {
			String key = entry.getKey().toString();

			key = prefix + key;

			setValue(key, entry.getValue().toString(), profile);
		}
	}

	// ---------------------------------------------------------------- initialize

	/**
	 * Initializes props. By default it only resolves active profiles.
	 */
	protected void initialize() {
		if (!initialized) {
			synchronized (this) {
				if (!initialized) {

					resolveActiveProfiles();

					initialized = true;
				}
			}
		}
	}

	/**
	 * Resolves active profiles from special property.
	 * This property can be only a base property!
	 * If default active property is not defined, nothing happens.
	 * Otherwise, it will replace currently active profiles.
	 */
	protected void resolveActiveProfiles() {
		if (activeProfilesProp == null) {
			activeProfiles = null;
			return;
		}

		final PropsEntry pv = data.getBaseProperty(activeProfilesProp);
		if (pv == null) {
			// no active profile set as the property, exit
			return;
		}

		final String value = pv.getValue();
		if (StringUtil.isBlank(value)) {
			activeProfiles = null;
			return;
		}

		activeProfiles = StringUtil.splitc(value, ',');
		StringUtil.trimAll(activeProfiles);
	}

	// ---------------------------------------------------------------- iterator

	/**
	 * Returns all profiles names.
	 */
	public String[] getAllProfiles() {
		String[] profiles = new String[data.profileProperties.size()];

		int index = 0;
		for (String profileName : data.profileProperties.keySet()) {
			profiles[index] = profileName;
			index++;
		}
		return profiles;
	}

	/**
	 * Returns all the profiles that define certain prop's key name.
	 * Key name is given as a wildcard, or it can be matched fully.
	 */
	public String[] getProfilesFor(final String propKeyNameWildcard) {
		HashSet<String> profiles = new HashSet<>();

		profile:
		for (Map.Entry<String, Map<String, PropsEntry>> entries : data.profileProperties.entrySet()) {
			String profileName = entries.getKey();

			Map<String, PropsEntry> value = entries.getValue();

			for (String propKeyName : value.keySet()) {
				if (Wildcard.equalsOrMatch(propKeyName, propKeyNameWildcard)) {
					profiles.add(profileName);
					continue profile;
				}
			}
		}

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

	/**
	 * Returns {@link PropsEntries builder} for entries {@link #iterator() itertor}.
	 */
	public PropsEntries entries() {
		initialize();
		return new PropsEntries(this);
	}

	/**
	 * Returns iterator for active profiles.
	 */
	public Iterator<PropsEntry> iterator() {
		return entries().activeProfiles().iterator();
	}

}


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

package jodd.petite;

import jodd.petite.def.CtorInjectionPoint;
import jodd.petite.def.DestroyMethodPoint;
import jodd.petite.def.InitMethodPoint;
import jodd.petite.def.MethodInjectionPoint;
import jodd.petite.def.PropertyInjectionPoint;
import jodd.petite.def.SetInjectionPoint;
import jodd.petite.def.ValueInjectionPoint;
import jodd.petite.scope.Scope;
import jodd.util.ArraysUtil;

import java.util.function.Consumer;

/**
 * Petite bean definition and cache. Consist of bean data that defines a bean
 * and cache, that might not be initialized (if null).
 * To initialize cache, get the bean instance from container.
 */
public class BeanDefinition<T> {

	public BeanDefinition(final String name, final Class<T> type, final Scope scope, final WiringMode wiringMode, final Consumer<T> beanInitConsumer) {
		this.name = name;
		this.type = type;
		this.scope = scope;
		this.wiringMode = wiringMode;
		this.consumer = beanInitConsumer;
	}

	// finals
	protected final String name;		    // bean name
	protected final Class<T> type;			// bean type
	protected final Scope scope;  		    // bean scope, may be null for beans that are not stored in scope but just wired
	protected final WiringMode wiringMode;	// wiring mode
	protected final Consumer<T> consumer;   // bean consumer, may be null

	// cache
	protected CtorInjectionPoint ctor;
	protected PropertyInjectionPoint[] properties;
	protected SetInjectionPoint[] sets;
	protected MethodInjectionPoint[] methods;
	protected InitMethodPoint[] initMethods;
	protected DestroyMethodPoint[] destroyMethods;
	protected String[] params;
	protected ValueInjectionPoint[] values;

	// ---------------------------------------------------------------- definition getters

	/**
	 * Returns bean name.
	 */
	public String name() {
		return name;
	}

	/**
	 * Returns bean type.
	 */
	public Class<T> type() {
		return type;
	}

	/**
	 * Returns beans scope type.
	 */
	public Class<? extends Scope> scope() {
		if (scope == null) {
			return null;
		}
		return scope.getClass();
	}

	/**
	 * Returns wiring mode.
	 */
	public WiringMode wiringMode() {
		return wiringMode;
	}

	/**
	 * Returns an optional consumer.
	 */
	public Consumer<T> consumer() {
		return consumer;
	}

	// ---------------------------------------------------------------- cache getters

	/**
	 * Returns constructor injection point.
	 */
	public CtorInjectionPoint ctorInjectionPoint() {
		return ctor;
	}

	/**
	 * Returns property injection points.
	 */
	public PropertyInjectionPoint[] propertyInjectionPoints() {
		return properties;
	}

	/**
	 * Returns set injection points.
	 */
	public SetInjectionPoint[] setterInjectionPoints() {
		return sets;
	}

	/**
	 * Returns method injection points.
	 */
	public MethodInjectionPoint[] methodInjectionPoints() {
		return methods;
	}

	/**
	 * Returns init method points.
	 */
	public InitMethodPoint[] initMethodPoints() {
		return initMethods;
	}

	/**
	 * Returns destroy method points.
	 */
	public DestroyMethodPoint[] destroyMethodPoints() {
		return destroyMethods;
	}

	/**
	 * Returns parameters.
	 */
	public String[] params() {
		return params;
	}

	// ---------------------------------------------------------------- scope delegates

	/**
	 * Delegates to {@link jodd.petite.scope.Scope#lookup(String)}.
	 */
	protected Object scopeLookup() {
		if (scope == null) {
			throw new PetiteException("Scope not defined");
		}
		return scope.lookup(name);
	}

	/**
	 * Delegates to {@link jodd.petite.scope.Scope#register(jodd.petite.BeanDefinition, Object)}
	 * if scope is defined.
	 */
	protected void scopeRegister(final Object object) {
		if (scope != null) {
			scope.register(this, object);
		}
	}

	/**
	 * Delegates to {@link jodd.petite.scope.Scope#remove(String)}.
	 */
	protected void scopeRemove() {
		scope.remove(name);
	}

	// ---------------------------------------------------------------- appends

	/**
	 * Adds property injection point.
	 */
	protected void addPropertyInjectionPoint(final PropertyInjectionPoint pip) {
		if (properties == null) {
			properties = new PropertyInjectionPoint[1];
			properties[0] = pip;
		} else {
			properties = ArraysUtil.append(properties, pip);
		}
	}

	/**
	 * Adds set injection point.
	 */
	protected void addSetInjectionPoint(final SetInjectionPoint sip) {
		if (sets == null) {
			sets = new SetInjectionPoint[1];
			sets[0] = sip;
		} else {
			sets = ArraysUtil.append(sets, sip);
		}
	}

	/**
	 * Adds method injection point.
	 */
	protected void addMethodInjectionPoint(final MethodInjectionPoint mip) {
		if (methods == null) {
			methods = new MethodInjectionPoint[1];
			methods[0] = mip;
		} else {
			methods = ArraysUtil.append(methods, mip);
		}
	}

	/**
	 * Adds init methods.
	 */
	protected void addInitMethodPoints(final InitMethodPoint[] methods) {
		if (initMethods == null) {
			initMethods = methods;
		} else {
			initMethods = ArraysUtil.join(initMethods, methods);
		}
	}

	/**
	 * Adds destroy methods.
	 */
	protected void addDestroyMethodPoints(final DestroyMethodPoint[] methods) {
		if (destroyMethods == null) {
			destroyMethods = methods;
		} else {
			destroyMethods = ArraysUtil.join(destroyMethods, methods);
		}
	}

	// ---------------------------------------------------------------- toString

	@Override
	public String toString() {
		return "BeanDefinition{" +
				"name=" + name +
				", type=" + type +
				", scope=" + (scope != null ? scope.getClass().getSimpleName() : "N/A") +
				", wiring=" + wiringMode +
				'}';
	}
}


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

package jodd.proxetta;

import jodd.proxetta.impl.WrapperProxettaFactory;

import java.lang.reflect.Field;

/**
 * Proxetta utilities.
 */
public class ProxettaUtil {

	/**
	 * Returns target class if proxetta applied on given class.
	 * If not, returns given class as result.
	 */
	public static Class resolveTargetClass(final Class proxy) {
		final String name = proxy.getName();

		if (name.endsWith(ProxettaNames.proxyClassNameSuffix)) {
			return proxy.getSuperclass();
		}

		if (name.endsWith(ProxettaNames.wrapperClassNameSuffix)) {
			return getTargetWrapperType(proxy);
		}

		return proxy;
	}

	/**
	 * Injects some target instance into {@link jodd.proxetta.impl.WrapperProxetta wrapper} proxy
	 * in given {@link WrapperProxettaFactory#setTargetFieldName(String) target field name}.
	 */
	public static void injectTargetIntoWrapper(final Object target, final Object wrapper, final String targetFieldName) {
		try {
			final Field field = wrapper.getClass().getField(targetFieldName);
			field.setAccessible(true);
			field.set(wrapper, target);
		} catch (Exception ex) {
			throw new ProxettaException(ex);
		}
	}

	/**
	 * Injects target instance into proxy using default target field name.
	 * @see #injectTargetIntoWrapper(Object, Object, String)
	 */
	public static void injectTargetIntoWrapper(final Object target, final Object wrapper) {
		injectTargetIntoWrapper(target, wrapper, ProxettaNames.wrapperTargetFieldName);
	}

	/**
	 * Returns wrapper target type.
	 */
	public static Class getTargetWrapperType(final Class wrapperClass) {
		try {
			final Field field = wrapperClass.getDeclaredField(ProxettaNames.wrapperTargetFieldName);
			return field.getType();
		} catch (NoSuchFieldException nsfex) {
			throw new ProxettaException(nsfex);
		}
	}

}


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

import java.util.HashMap;
import java.util.Iterator;

/**
 * LFU (least frequently used) cache. Frequency is calculated as access count. This cache
 * is resistant on 'new usages scenario': when some object is removed from the cache,
 * access count of all items in cache is decreased by access count of removed value.
 * This allows new frequent elements to come into the cache.
 *

 * Frequency of use data is kept on all items. The most frequently used items are kept in the cache.
 * Because of the bookkeeping requirements, cache access overhead increases logarithmically with cache size.
 * The advantage is that long term usage patterns are captured well, incidentally making the algorithm scan resistant;
 * the disadvantage, besides the larger access overhead, is that the algorithm doesn't adapt quickly to changing
 * usage patterns, and in particular doesn't help with temporally clustered accesses.
 *

 * Summary for LFU: not fast, captures frequency of use, scan resistant.
 */
public class LFUCache<K,V> extends AbstractCacheMap<K,V> {

	public LFUCache(final int maxSize) {
		this(maxSize, 0);
	}

	public LFUCache(final int maxSize, final long timeout) {
		this.cacheSize = maxSize;
		this.timeout = timeout;
		cacheMap = new HashMap<>(maxSize + 1);
	}

	// ---------------------------------------------------------------- prune

	/**
	 * Prunes expired and, if cache is still full, the LFU element(s) from the cache.
	 * On LFU removal, access count is normalized to value which had removed object.
	 * Returns the number of removed objects.
	 */
	@Override
	protected int pruneCache() {
        int count = 0;
		CacheObject<K,V> comin = null;

		// remove expired items and find cached object with minimal access count
		Iterator<CacheObject<K,V>> values = cacheMap.values().iterator();
		while (values.hasNext()) {
			CacheObject<K,V> co = values.next();
			if (co.isExpired()) {
				values.remove();
				onRemove(co.key, co.cachedObject);
				count++;
				continue;
			}

			if (comin == null) {
				comin = co;
			} else {
				if (co.accessCount < comin.accessCount) {
					comin = co;
				}
			}
		}

		if (!isFull()) {
			return count;
		}

		// decrease access count to all cached objects
		if (comin != null) {
			long minAccessCount = comin.accessCount;

			values = cacheMap.values().iterator();
			while (values.hasNext()) {
				CacheObject<K, V> co = values.next();
				co.accessCount -= minAccessCount;
				if (co.accessCount <= 0) {
					values.remove();
					onRemove(co.key, co.cachedObject);
					count++;
				}
			}
		}
		return count;
	}

}


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

/**
 * Property descriptor. It consist of read, write and field descriptor.
 * Only one of those three descriptors may exist.
 */
public class PropertyDescriptor extends Descriptor {

	protected final String name;
	protected final MethodDescriptor readMethodDescriptor;
	protected final MethodDescriptor writeMethodDescriptor;
	protected final FieldDescriptor fieldDescriptor;

	/**
	 * Creates field-only property descriptor.
	 */
	public PropertyDescriptor(final ClassDescriptor classDescriptor, final String propertyName, final FieldDescriptor fieldDescriptor) {
		super(classDescriptor, false);
		this.name = propertyName;
		this.readMethodDescriptor = null;
		this.writeMethodDescriptor = null;
		this.fieldDescriptor = fieldDescriptor;
	}

	/**
	 * Creates property descriptor.
	 */
	public PropertyDescriptor(final ClassDescriptor classDescriptor, final String propertyName, final MethodDescriptor readMethod, final MethodDescriptor writeMethod) {
		super(classDescriptor,
				((readMethod == null) || readMethod.isPublic()) & (writeMethod == null || writeMethod.isPublic())
		);
		this.name = propertyName;
		this.readMethodDescriptor = readMethod;
		this.writeMethodDescriptor = writeMethod;

		if (classDescriptor.isExtendedProperties()) {
			String[] prefix = classDescriptor.getPropertyFieldPrefix();

			FieldDescriptor fd = null;

			if (prefix != null) {
				for (String p : prefix) {
					fd = findField(p + propertyName);

					if (fd != null) {
						break;
					}
				}
			}
			else {
				fd = findField(propertyName);
			}

			this.fieldDescriptor = fd;
		} else {
			this.fieldDescriptor = null;
		}
	}

	/**
	 * Locates property field. Field is being searched also in all
	 * superclasses of current class.
	 */
	protected FieldDescriptor findField(final String fieldName) {
		FieldDescriptor fieldDescriptor = classDescriptor.getFieldDescriptor(fieldName, true);

		if (fieldDescriptor != null) {
			return fieldDescriptor;
		}

		// field descriptor not found in this class
		// try to locate it in the superclasses

		Class[] superclasses = classDescriptor.getAllSuperclasses();

		for (Class superclass : superclasses) {

			ClassDescriptor classDescriptor = ClassIntrospector.get().lookup(superclass);

			fieldDescriptor = classDescriptor.getFieldDescriptor(fieldName, true);

			if (fieldDescriptor != null) {
				return fieldDescriptor;
			}
		}

		// nothing found
		return null;
	}

	/**
	 * Returns property name.
	 */
	@Override
	public String getName() {
		return name;
	}

	/**
	 * Returns read method of this property.
	 * May be null if read method is not defined.
	 */
	public MethodDescriptor getReadMethodDescriptor() {
		return readMethodDescriptor;
	}

	/**
	 * Returns write method of this property.
	 * May be null for read-only properties.
	 */
	public MethodDescriptor getWriteMethodDescriptor() {
		return writeMethodDescriptor;
	}

	/**
	 * Returns the associated field of this property.
	 * May be null if properties are not enhanced by field description.
	 */
	public FieldDescriptor getFieldDescriptor() {
		return fieldDescriptor;
	}

	/**
	 * Returns true if this is an extended property with
	 * only field definition and without getter and setter.
	 */
	public boolean isFieldOnly() {
		return (readMethodDescriptor == null) && (writeMethodDescriptor == null);
	}

	/**
	 * Returns true if this property has only a getter method.
	 */
	public boolean isGetterOnly() {
		return (fieldDescriptor == null) && (writeMethodDescriptor == null);
	}

	/**
	 * Returns true if this property has only a setter method.
	 */
	public boolean isSetterOnly() {
		return (fieldDescriptor == null) && (readMethodDescriptor == null);
	}

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

	protected Class type;

	/**
	 * Returns property type. Raw types are detected.
	 */
	public Class getType() {
		if (type == null) {
			if (fieldDescriptor != null) {
				type = fieldDescriptor.getRawType();
			}
			else if (readMethodDescriptor != null) {
				type = getGetter(true).getGetterRawType();
				//type = readMethodDescriptor.getGetterRawType();
			}
			else if (writeMethodDescriptor != null) {
				type = getSetter(true).getSetterRawType();
				//type = writeMethodDescriptor.getSetterRawType();
			}
		}

		return type;
	}

	// ---------------------------------------------------------------- getters & setters

	protected Getter[] getters;
	protected Setter[] setters;

	/**
	 * Returns {@link Getter}. May return null
	 * if no matched getter is found.
	 */
	public Getter getGetter(final boolean declared) {
		if (getters == null) {
			getters = new Getter[] {
					createGetter(false),
					createGetter(true),
			};
		}

		return getters[declared ? 1 : 0];
	}

	/**
	 * Creates a {@link Getter}.
	 */
	protected Getter createGetter(final boolean declared) {
		if (readMethodDescriptor != null) {
			if (readMethodDescriptor.matchDeclared(declared)) {
				return Getter.of(readMethodDescriptor);
			}
		}
		if (fieldDescriptor != null) {
			if (fieldDescriptor.matchDeclared(declared)) {
				return Getter.of(fieldDescriptor);
			}
		}
		return null;
	}

	/**
	 * Returns {@link Setter}. May return null
	 * if no matched setter is found.
	 */
	public Setter getSetter(final boolean declared) {
		if (setters == null) {
			setters = new Setter[] {
					createSetter(false),
					createSetter(true),
			};
		}

		return setters[declared ? 1 : 0];
	}

	/**
	 * Creates a {@link Setter}.
	 */
	protected Setter createSetter(final boolean declared) {
		if (writeMethodDescriptor != null) {
			if (writeMethodDescriptor.matchDeclared(declared)) {
				return Setter.of(writeMethodDescriptor);
			}
		}
		if (fieldDescriptor != null) {
			if (fieldDescriptor.matchDeclared(declared)) {
				return Setter.of(fieldDescriptor);
			}
		}
		return null;
	}

	// ---------------------------------------------------------------- resolvers

	/**
	 * Resolves key type for given property descriptor.
	 */
	public Class resolveKeyType(final boolean declared) {
		Class keyType = null;

		Getter getter = getGetter(declared);

		if (getter != null) {
			keyType = getter.getGetterRawKeyComponentType();
		}

		if (keyType == null) {
			FieldDescriptor fieldDescriptor = getFieldDescriptor();

			if (fieldDescriptor != null) {
				keyType = fieldDescriptor.getRawKeyComponentType();
			}
		}

		return keyType;
	}

	/**
	 * Resolves component type for given property descriptor.
	 */
	public Class resolveComponentType(final boolean declared) {
		Class componentType = null;

		Getter getter = getGetter(declared);

		if (getter != null) {
			componentType = getter.getGetterRawComponentType();
		}

		if (componentType == null) {
			FieldDescriptor fieldDescriptor = getFieldDescriptor();

			if (fieldDescriptor != null) {
				componentType = fieldDescriptor.getRawComponentType();
			}
		}

		return componentType;
	}

}


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

package jodd.system;

import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * Host information.
 */
abstract class HostInfo {

	private final String HOST_NAME;
	private final String HOST_ADDRESS;

	public HostInfo() {
		String hostName;
		String hostAddress;

		try {
			final InetAddress localhost = InetAddress.getLocalHost();

			hostName = localhost.getHostName();
			hostAddress = localhost.getHostAddress();
		}
		catch (UnknownHostException uhex) {
			hostName = "localhost";
			hostAddress = "127.0.0.1";
		}

		this.HOST_NAME = hostName;
		this.HOST_ADDRESS = hostAddress;
	}

	/**
	 * Returns host name.
	 */
	public final String getHostName() {
		return HOST_NAME;
	}

	/**
	 * Returns host IP address.
	 */
	public final String getHostAddress() {
		return HOST_ADDRESS;
	}

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

	protected String nosep(final String in) {
		if (in.endsWith(File.separator)) {
			return in.substring(0, in.length() - 1);
		}
		return in;
	}

	// ---------------------------------------------------------------- toString

	@Override
	public String toString() {
		return  "\nHost name:    " + getHostName() +
				"\nHost address: " + getHostAddress();
	}

}


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

package jodd.madvoc.result;

import jodd.exception.ExceptionUtil;
import jodd.json.JsonSerializer;
import jodd.madvoc.meta.RenderWith;
import jodd.util.StringPool;
import jodd.net.HttpStatus;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * General JSON result.
 */
@RenderWith(JsonActionResult.class)
public class JsonResult {

	private final String body;
	private int status = 200;
	private String message = "OK";

	/**
	 * Creates new JSON result of provided JSON string.
	 */
	public static JsonResult of(final String json) {
		return new JsonResult(json);
	}

	/**
	 * Creates JSON result from given object. The object will be serialized to JSON.
	 */
	public static JsonResult of(final Object object) {
		final String json = JsonSerializer.create().deep(true).serialize(object);
		return new JsonResult(json);
	}

	/**
	 * Creates a JSON result with empty body and given status.
	 */
	public static JsonResult of(final HttpStatus httpStatus) {
		return empty().status(httpStatus);
	}

	/**
	 * Creates a JSON response from an exception. Response body will have information about the
	 * exception and response status will be set to 500.
	 */
	public static JsonResult of(final Exception exception) {
		final HashMap<String, Object> errorMap = new HashMap<>();

		errorMap.put("message", ExceptionUtil.message(exception));
		errorMap.put("error", exception.getClass().getName());
		errorMap.put("cause", exception.getCause() != null ? exception.getCause().getClass().getName() : null);

		final ArrayList<String> details = new ArrayList<>();

		final StackTraceElement[] ste = ExceptionUtil.getStackTrace(exception, null, null);
		for (StackTraceElement stackTraceElement : ste) {
			details.add(stackTraceElement.toString());
		}

		errorMap.put("details", details);

		final String json = JsonSerializer.create().deep(true).serialize(errorMap);
		return new JsonResult(json).status(HttpStatus.error500().internalError());
	}

	/**
	 * Creates a JSON result with empty body.
	 */
	public static JsonResult empty() {
		return new JsonResult(StringPool.EMPTY);
	}

	public JsonResult(final String body) {
		this.body = body;
	}

	public JsonResult status(final int status) {
		this.status = status;
		return this;
	}

	public JsonResult status(final HttpStatus httpStatus) {
		this.status = httpStatus.status();
		this.message = httpStatus.message();
		return this;
	}

	public JsonResult status(final int status, final String message) {
		this.status = status;
		this.message = message;
		return this;
	}

	/**
	 * Returns JSON body.
	 */
	public String value() {
		return body;
	}

	/**
	 * Returns response status.
	 */
	public int status() {
		return status;
	}

	/**
	 * Returns response message.
	 */
	public String message() {
		return message;
	}
}


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

package jodd.madvoc.path;

import jodd.madvoc.component.ActionsManager;
import jodd.madvoc.config.ActionDefinition;
import jodd.madvoc.config.ActionNames;
import jodd.madvoc.meta.In;
import jodd.madvoc.meta.scope.MadvocContext;
import jodd.util.CharUtil;
import jodd.util.StringPool;
import jodd.net.HttpMethod;

import java.lang.reflect.Method;

/**
 * Naming strategy for REST resources.
 */
public class RestActionNamingStrategy extends BaseNamingStrategy {

	@In @MadvocContext
	protected ActionsManager actionsManager;

	protected boolean includeMethodActionPath = true;

	@Override
	public ActionDefinition buildActionDef(final Class actionClass, final Method actionMethod, final ActionNames actionNames) {

		final String packageActionPath = actionNames.packageActionPath();
		final String classActionPath = actionNames.classActionPath();

		String methodActionPath = includeMethodActionPath ? actionNames.methodActionPath() : null;
		String httpMethod = actionNames.httpMethod();

		if (httpMethod == null) {
			httpMethod = resolveHttpMethodFromMethodName(actionMethod.getName());
		}

		String actionPath = classActionPath;
		String resultPath = classActionPath;

		if (isAbsolutePath(methodActionPath)) {
			return createActionDef(methodActionPath, httpMethod, methodActionPath, actionNames);
		}

		if (methodActionPath != null) {
			if (httpMethod == null && methodActionPath.startsWith(actionsManager.getPathMacroSeparators()[0])) {
				methodActionPath = actionMethod.getName() + StringPool.SLASH + methodActionPath;
			}

			if (!classActionPath.endsWith(StringPool.SLASH)) {
				actionPath += StringPool.SLASH;
				resultPath += StringPool.SLASH;
			}

			actionPath += methodActionPath;

			if (httpMethod != null) {
				resultPath += httpMethod.toLowerCase();
			} else {
				resultPath += actionNames.methodName();
			}
		}

		if (isAbsolutePath(actionPath)) {
			return createActionDef(actionPath, httpMethod, resultPath, actionNames);
		}

		if (packageActionPath != null) {
			actionPath = packageActionPath + actionPath;
			resultPath = packageActionPath + resultPath;
		} else {
			actionPath = StringPool.SLASH + actionPath;
			resultPath = StringPool.SLASH + resultPath;
		}

		return createActionDef(actionPath, httpMethod, resultPath, actionNames);
	}

	/**
	 * Resolves HTTP method name from method name.
	 * If method name or first camel-case word of a method equals to
	 * a HTTP method, it will be used as that HTTP methods.
	 */
	protected String resolveHttpMethodFromMethodName(final String methodName) {
		int i = 0;

		while (i < methodName.length()) {
			if (CharUtil.isUppercaseAlpha(methodName.charAt(i))) {
				break;
			}
			i++;
		}

		final String name = methodName.substring(0, i).toUpperCase();

		for (final HttpMethod httpMethod : HttpMethod.values()) {
			if (httpMethod.equalsName(name)) {
				return httpMethod.name();
			}
		}

		return null;
	}

}


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

package jodd.util;

/**
 * Checks whether a string or path matches a given wildcard pattern.
 * Possible patterns allow to match single characters ('?') or any count of
 * characters ('*'). Wildcard characters can be escaped (by an '\').
 * When matching path, deep tree wildcard also can be used ('**').
 *

 * This method uses recursive matching, as in linux or windows. regexp works the same.
 * This method is very fast, comparing to similar implementations.
 */
public class Wildcard {

	/**
	 * Checks whether a string matches a given wildcard pattern.
	 *
	 * @param string	input string
	 * @param pattern	pattern to match
	 * @return 			true if string matches the pattern, otherwise false
	 */
	public static boolean match(final CharSequence string, final CharSequence pattern) {
		return match(string, pattern, 0, 0);
	}

	/**
	 * Checks if two strings are equals or if they {@link #match(CharSequence, CharSequence)}.
	 * Useful for cases when matching a lot of equal strings and speed is important.
	 */
	public static boolean equalsOrMatch(final CharSequence string, final CharSequence pattern) {
		if (string.equals(pattern)) {
			return true;
		}
		return match(string, pattern, 0, 0);
	}

	/**
	 * Internal matching recursive function.
	 */
	private static boolean match(final CharSequence string, final CharSequence pattern, int sNdx, int pNdx) {
		int pLen = pattern.length();
		if (pLen == 1) {
			if (pattern.charAt(0) == '*') {     // speed-up
				return true;
			}
		}
		int sLen = string.length();
		boolean nextIsNotWildcard = false;

		while (true) {

			// check if end of string and/or pattern occurred
			if ((sNdx >= sLen)) {		// end of string still may have pending '*' in pattern
				while ((pNdx < pLen) && (pattern.charAt(pNdx) == '*')) {
					pNdx++;
				}
				return pNdx >= pLen;
			}
			if (pNdx >= pLen) {					// end of pattern, but not end of the string
				return false;
			}
			char p = pattern.charAt(pNdx);		// pattern char

			// perform logic
			if (!nextIsNotWildcard) {

				if (p == '\\') {
					pNdx++;
					nextIsNotWildcard =  true;
					continue;
				}
				if (p == '?') {
					sNdx++; pNdx++;
					continue;
				}
				if (p == '*') {
					char pNext = 0;						// next pattern char
					if (pNdx + 1 < pLen) {
						pNext = pattern.charAt(pNdx + 1);
					}
					if (pNext == '*') {					// double '*' have the same effect as one '*'
						pNdx++;
						continue;
					}
					int i;
					pNdx++;

					// find recursively if there is any substring from the end of the
					// line that matches the rest of the pattern !!!
					for (i = string.length(); i >= sNdx; i--) {
						if (match(string, pattern, i, pNdx)) {
							return true;
						}
					}
					return false;
				}
			} else {
				nextIsNotWildcard = false;
			}

			// check if pattern char and string char are equals
			if (p != string.charAt(sNdx)) {
				return false;
			}

			// everything matches for now, continue
			sNdx++; pNdx++;
		}
	}

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

	/**
	 * Matches string to at least one pattern.
	 * Returns index of matched pattern, or -1 otherwise.
	 * @see #match(CharSequence, CharSequence)
	 */
	public static int matchOne(final String src, final String... patterns) {
		for (int i = 0; i < patterns.length; i++) {
			if (match(src, patterns[i])) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Matches path to at least one pattern.
	 * Returns index of matched pattern or -1 otherwise.
	 * @see #matchPath(String, String)
	 */
	public static int matchPathOne(final String path, final String... patterns) {
		for (int i = 0; i < patterns.length; i++) {
			if (matchPath(path, patterns[i])) {
				return i;
			}
		}
		return -1;
	}

	// ---------------------------------------------------------------- path

	protected static final String PATH_MATCH = "**";
	protected static final String PATH_SEPARATORS = "/\\";

	/**
	 * Matches path against pattern using *, ? and ** wildcards.
	 * Both path and the pattern are tokenized on path separators (both \ and /).
	 * '**' represents deep tree wildcard, as in Ant.
	 */
	public static boolean matchPath(final String path, final String pattern) {
		String[] pathElements = StringUtil.splitc(path, PATH_SEPARATORS);
		String[] patternElements = StringUtil.splitc(pattern, PATH_SEPARATORS);
		return matchTokens(pathElements, patternElements);
	}

	/**
	 * Match tokenized string and pattern.
	 */
	protected static boolean matchTokens(final String[] tokens, final String[] patterns) {
		int patNdxStart = 0;
		int patNdxEnd = patterns.length - 1;
		int tokNdxStart = 0;
		int tokNdxEnd = tokens.length - 1;

		while ((patNdxStart <= patNdxEnd) && (tokNdxStart <= tokNdxEnd)) {	// find first **
			String patDir = patterns[patNdxStart];
			if (patDir.equals(PATH_MATCH)) {
				break;
			}
			if (!match(tokens[tokNdxStart], patDir)) {
				return false;
			}
			patNdxStart++;
			tokNdxStart++;
		}
		if (tokNdxStart > tokNdxEnd) {
			for (int i = patNdxStart; i <= patNdxEnd; i++) {	// string is finished
				if (!patterns[i].equals(PATH_MATCH)) {
					return false;
				}
			}
			return true;
		}
		if (patNdxStart > patNdxEnd) {
			return false;	// string is not finished, but pattern is
		}

		while ((patNdxStart <= patNdxEnd) && (tokNdxStart <= tokNdxEnd)) {	// to the last **
			String patDir = patterns[patNdxEnd];
			if (patDir.equals(PATH_MATCH)) {
				break;
			}
			if (!match(tokens[tokNdxEnd], patDir)) {
				return false;
			}
			patNdxEnd--;
			tokNdxEnd--;
		}
		if (tokNdxStart > tokNdxEnd) {
			for (int i = patNdxStart; i <= patNdxEnd; i++) {	// string is finished
				if (!patterns[i].equals(PATH_MATCH)) {
					return false;
				}
			}
			return true;
		}

		while ((patNdxStart != patNdxEnd) && (tokNdxStart <= tokNdxEnd)) {
			int patIdxTmp = -1;
			for (int i = patNdxStart + 1; i <= patNdxEnd; i++) {
				if (patterns[i].equals(PATH_MATCH)) {
					patIdxTmp = i;
					break;
				}
			}
			if (patIdxTmp == patNdxStart + 1) {
				patNdxStart++;	// skip **/** situation
				continue;
			}
			// find the pattern between padIdxStart & padIdxTmp in str between strIdxStart & strIdxEnd
			int patLength = (patIdxTmp - patNdxStart - 1);
			int strLength = (tokNdxEnd - tokNdxStart + 1);
			int ndx = -1;
			strLoop:
			for (int i = 0; i <= strLength - patLength; i++) {
				for (int j = 0; j < patLength; j++) {
					String subPat = patterns[patNdxStart + j + 1];
					String subStr = tokens[tokNdxStart + i + j];
					if (!match(subStr, subPat)) {
						continue strLoop;
					}
				}

				ndx = tokNdxStart + i;
				break;
			}

			if (ndx == -1) {
				return false;
			}

			patNdxStart = patIdxTmp;
			tokNdxStart = ndx + patLength;
		}

		for (int i = patNdxStart; i <= patNdxEnd; i++) {
			if (!patterns[i].equals(PATH_MATCH)) {
				return false;
			}
		}

		return true;
	}
}


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

package jodd.util.cl;

import jodd.system.SystemUtil;
import jodd.util.ArraysUtil;
import jodd.util.StringUtil;
import jodd.util.Wildcard;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

/**
 * Class loader that offers two loading strategies: parent-first and parent-last.
 * The strategy defines the class loading order between this classloader and parent class loader;
 * i.e. defines the first class loader to be used for loading classes.
 * Extends URLClassLoader to provide just minimal set of modifications.
 *

 * Either way, you can specify wildcard rules to force some behavior - like loading some package/class
 * exclusively with specific loader. There are the following groups:
 *

 *
parent-only - classes will be loaded only with parent classloader.

 *
loader-only - classes will be loaded only with this loader.

 *
default - classes will be loaded as specified by strategy (no need to define).

 *

 *

 * The order of matching is also set by loading strategy. For parent-first strategy,
 * loader group rules will be checked first. For parent-last strategy,
 * parent group rules will be checked first.
 *

 * By default, the list of parent-only group is populated with
 * {@link jodd.system.SystemInfo#getJrePackages()}  JRE packages}.
 *

 * When parent-last strategy is used, be aware how you use
 * {@link jodd.util.ClassLoaderUtil} as it is designed to follow parent-first
 * strategy. Use with caution as setting parent-last violates the
 * class loader hierarchy.
 */
public class ExtendedURLClassLoader extends URLClassLoader {

	protected final ClassLoader parentClassLoader;
	protected String[] parentOnlyRules;
	protected String[] loaderOnlyRules;
	protected final boolean parentFirst;
	protected boolean matchResourcesAsPackages = true;

	public ExtendedURLClassLoader(final URL[] classpath, final ClassLoader parent, final boolean parentFirst) {
		this(classpath, parent, parentFirst, true);
	}

	/**
	 * Creates class loader with given loading strategy.
	 */
	public ExtendedURLClassLoader(
		final URL[] classpath, final ClassLoader parent,
		final boolean parentFirst, final boolean excludeJrePackagesFromLoader) {

		super(classpath, parent);

		this.parentFirst = parentFirst;

		if (parent == null) {
			throw new IllegalArgumentException("Parent classloader not specified");
		}
		parentClassLoader = parent;

		parentOnlyRules = new String[0];
		loaderOnlyRules = new String[0];

		if (excludeJrePackagesFromLoader) {
			final String[] corePackages = SystemUtil.info().getJrePackages();

			for (String corePackage : corePackages) {
				if (corePackage.equals("javax")) {
					// javax is NOT forbidden
					continue;
				}

				addParentOnlyRules(corePackage + ".*");
			}
		}
	}

	// ---------------------------------------------------------------- rules

	/**
	 * Adds parent only rules for classes which must be loaded on the
	 * parent loader.
	 */
	public void addParentOnlyRules(final String... packages) {
		parentOnlyRules = ArraysUtil.join(parentOnlyRules, packages);
	}

	/**
	 * Adds loader-only rules for classes which must be loaded using this
	 * loader.
	 */
	public void addLoaderOnlyRules(final String... packages) {
		loaderOnlyRules = ArraysUtil.join(loaderOnlyRules, packages);
	}

	/**
	 * When set, resources will be matched in the same way as packages.
	 * If disabled, resources must be matched only with separate rules
	 * that uses "/".
	 */
	public void setMatchResourcesAsPackages(final boolean matchResourcesAsPackages) {
		this.matchResourcesAsPackages = matchResourcesAsPackages;
	}

	/**
	 * Returns true if class or resource name matches
	 * at least one package rule from the list.
	 */
	protected boolean isMatchingRules(final String name, final String... rules) {
		for (String rule : rules) {
			if (Wildcard.equalsOrMatch(name, rule)) {
				return true;
			}
		}
		return false;
	}

	// ---------------------------------------------------------------- resolver

	protected static class Loading {
		public Loading(final boolean withParent, final boolean withLoader) {
			this.withParent = withParent;
			this.withLoader = withLoader;
		}

		protected final boolean withParent;
		protected final boolean withLoader;
	}

	/**
	 * Resolves loading rules.
	 */
	protected Loading resolveLoading(final boolean parentFirstStrategy, final String className) {
		boolean withParent = true;
		boolean withLoader = true;

		if (parentFirstStrategy) {
			if (isMatchingRules(className, loaderOnlyRules)) {
				withParent = false;
			}
			else if (isMatchingRules(className, parentOnlyRules)) {
				withLoader = false;
			}
		}
		else {
			if (isMatchingRules(className, parentOnlyRules)) {
				withLoader = false;
			}
			else if (isMatchingRules(className, loaderOnlyRules)) {
				withParent = false;
			}
		}

		return new Loading(withParent, withLoader);
	}

	/**
	 * Resolves resources.
	 */
	protected Loading resolveResourceLoading(final boolean parentFirstStrategy, String resourceName) {
		if (matchResourcesAsPackages) {
			resourceName = StringUtil.replaceChar(resourceName, '/', '.');
		}

		return resolveLoading(parentFirstStrategy, resourceName);
	}

	// ---------------------------------------------------------------- overrides

	/**
	 * Loads class using parent-first or parent-last strategy.
	 */
	@Override
	protected synchronized Class<?> loadClass(final String className, final boolean resolve) throws ClassNotFoundException {

		// check first if the class has already been loaded

		Class<?> c = findLoadedClass(className);

		if (c != null) {
			if (resolve) {
				resolveClass(c);
			}

			return c;
		}

		// class not loaded yet

		Loading loading = resolveLoading(parentFirst, className);

		if (parentFirst) {
			// PARENT FIRST
			if (loading.withParent) {
				try {
					c = parentClassLoader.loadClass(className);
				}
				catch (ClassNotFoundException ignore) {
				}
			}

			if (c == null) {
				if (loading.withLoader) {
					c = this.findClass(className);
				}
				else {
					throw new ClassNotFoundException("Class not found: " + className);
				}
			}
		} else {
			// THIS FIRST
			if (loading.withLoader) {
				try {
					c = this.findClass(className);
				}
				catch (ClassNotFoundException ignore) {
				}
			}

			if (c == null) {
				if (loading.withParent) {
					c = parentClassLoader.loadClass(className);
				}
				else {
					throw new ClassNotFoundException("Class not found: " + className);
				}
			}
		}

		if (resolve) {
			resolveClass(c);
		}
		return c;
	}

	/**
	 * Returns a resource using parent-first or parent-last strategy.
	 */
	@Override
	public URL getResource(final String resourceName) {

		URL url = null;

		Loading loading = resolveResourceLoading(parentFirst, resourceName);

		if (parentFirst) {
			// PARENT FIRST
			if (loading.withParent) {
				url = parentClassLoader.getResource(resourceName);
			}

			if (url == null) {
				if (loading.withLoader) {
					url = this.findResource(resourceName);
				}
			}
		} else {
			// THIS FIRST
			if (loading.withLoader) {
				url = this.findResource(resourceName);
			}

			if (url == null) {
				if (loading.withParent) {
					url = parentClassLoader.getResource(resourceName);
				}
			}
		}

		return url;
	}

	@Override
	public Enumeration<URL> getResources(final String resourceName) throws IOException {

		final List<URL> urls = new ArrayList<>();

		Enumeration<URL> loaderUrls = this.findResources(resourceName);
		Enumeration<URL> parentUrls = parentClassLoader.getResources(resourceName);

		Loading loading = resolveResourceLoading(parentFirst, resourceName);

		if (parentFirst) {
			if (loading.withParent) {
				while (parentUrls.hasMoreElements()) {
					urls.add(parentUrls.nextElement());
				}
			}
			if (loading.withLoader) {
				while (loaderUrls.hasMoreElements()) {
					urls.add(loaderUrls.nextElement());
				}
			}
		}
		else {
			if (loading.withLoader) {
				while (loaderUrls.hasMoreElements()) {
					urls.add(loaderUrls.nextElement());
				}
			}
			if (loading.withParent) {
				while (parentUrls.hasMoreElements()) {
					urls.add(parentUrls.nextElement());
				}
			}
		}
		return new Enumeration<URL>() {
			Iterator<URL> iterator = urls.iterator();

			@Override
			public boolean hasMoreElements() {
				return iterator.hasNext();
			}

			@Override
			public URL nextElement() {
				return iterator.next();
			}
		};
	}

}


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

package jodd.io;

import jodd.util.StringPool;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;

/**
 * Consumes a stream.
 * For any Process, the input and error streams must read even
 * if the data written to these streams is not used by the application.
 * The generally accepted solution for this problem is a stream gobbler thread
 * that does nothing but consume data from an input stream until stopped.
 */
public class StreamGobbler extends Thread {

	protected final InputStream is;
	protected final String prefix;
	protected final OutputStream out;
	protected final Object lock = new Object();
	protected boolean end = false;

	public StreamGobbler(final InputStream is) {
		this(is, null, null);
	}

	public StreamGobbler(final InputStream is, final OutputStream output) {
		this(is, output, null);
	}

	public StreamGobbler(final InputStream is, final OutputStream output, final String prefix) {
		this.is = is;
		this.prefix = prefix;
		this.out = output;
	}

	@Override
	public void run() {
		InputStreamReader isr = new InputStreamReader(is);
		BufferedReader br = new BufferedReader(isr);

		try {
			String line;
			while ((line = br.readLine()) != null) {
				if (out != null) {
					if (prefix != null) {
						out.write(prefix.getBytes());
					}
					out.write(line.getBytes());
					out.write(StringPool.BYTES_NEW_LINE);
				}
			}
		}
		catch (IOException ioe) {
			if (out != null) {
				ioe.printStackTrace(new PrintStream(out));
			}
		}
		finally {
			if (out != null) {
				try {
					out.flush();
				}
				catch (IOException ignore) {
				}
			}
			try {
				br.close();
			}
			catch (IOException ignore) {
			}
		}

		synchronized (lock) {
			lock.notifyAll();
			end = true;
		}
	}

	/**
	 * Waits for gobbler to end.
	 */
	public void waitFor() {
		try {
			synchronized (lock) {
				if (!end) {
					lock.wait();
				}
			}
		}
		catch (InterruptedException ignore) {
			Thread.currentThread().interrupt();
		}
	}

}