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

package jodd.mail;

import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import java.io.File;
import java.util.function.Consumer;

/**
 * Encapsulates {@link Email} receiving session. Prepares and receives {@link Email}s.
 * Some methods do not work on POP3 servers.
 */
public class ReceiveMailSession extends MailSession<Store> {

	/**
	 * Default folder.
	 */
	protected static final String DEFAULT_FOLDER = "INBOX";

	/**
	 * The current folder.
	 */
	Folder folder;
	String folderName;

	private final File attachmentStorage;

	static {
		setupSystemMailProperties();
	}

	/**
	 * Creates new mail session.
	 *
	 * @param session {@link Session}.
	 * @param store   {@link Store}.
	 */
	public ReceiveMailSession(final Session session, final Store store, final File attachmentStorage) {
		super(session, store);
		this.attachmentStorage = attachmentStorage;
	}

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

	// ---------------------------------------------------------------- folders

	/**
	 * Returns array of all {@link Folder}s as {@code String}s. You can use these names in
	 * {@link #useFolder(String)} method.
	 *
	 * @return array of all {@link Folder}s as {@code String}s.
	 */
	public String[] getAllFolders() {
		final Folder[] folders;
		try {
			folders = getService().getDefaultFolder().list("*");
		} catch (final MessagingException msgexc) {
			throw new MailException("Failed to connect to folder", msgexc);
		}
		final String[] folderNames = new String[folders.length];

		for (int i = 0; i < folders.length; i++) {
			final Folder folder = folders[i];
			folderNames[i] = folder.getFullName();
		}
		return folderNames;
	}

	/**
	 * Opens new folder and closes previously opened folder.
	 *
	 * @param folderName Folder to open
	 */
	public void useFolder(final String folderName) {
		closeFolderIfOpened(folder);

		try {
			this.folderName = folderName;
			this.folder = getService().getFolder(folderName);
			try {
				folder.open(Folder.READ_WRITE);
			} catch (final MailException ignore) {
				folder.open(Folder.READ_ONLY);
			}
		} catch (final MessagingException msgexc) {
			throw new MailException("Failed to connect to folder: " + folderName, msgexc);
		}
	}

	/**
	 * Just returns a folder, w/o opening.
	 */
	public Folder getFolder(final String folder) {
		try {
			return getService().getFolder(folder);
		} catch (MessagingException e) {
			throw new MailException("Folder not found: " + folder, e);
		}
	}

	// ---------------------------------------------------------------- open

	/**
	 * Opens default folder: DEFAULT_FOLDER.
	 */
	public void useDefaultFolder() {
		closeFolderIfOpened(folder);
		useFolder(DEFAULT_FOLDER);
	}

	private void useAndOpenFolderIfNotSet() {
		if (folder == null) {
			if (folderName != null) {
				useFolder(folderName);
			}
			else {
				useDefaultFolder();
			}
		}
	}

	// ---------------------------------------------------------------- message count

	/**
	 * Returns number of messages.
	 *
	 * @return The number of messages.
	 */
	public int getMessageCount() {
		useAndOpenFolderIfNotSet();
		try {
			return folder.getMessageCount();
		} catch (final MessagingException msgexc) {
			throw new MailException(msgexc);
		}
	}

	/**
	 * Returns the number of new messages.
	 *
	 * @return The number of new message.
	 */
	public int getNewMessageCount() {
		useAndOpenFolderIfNotSet();
		try {
			return folder.getNewMessageCount();
		} catch (final MessagingException msgexc) {
			throw new MailException(msgexc);
		}
	}

	/**
	 * Returns the number of unread messages.
	 */
	public int getUnreadMessageCount() {
		useAndOpenFolderIfNotSet();
		try {
			return folder.getUnreadMessageCount();
		} catch (final MessagingException msgexc) {
			throw new MailException(msgexc);
		}
	}

	/**
	 * Returns the number of deleted messages.
	 *
	 * @return The number of deleted messages.
	 */
	public int getDeletedMessageCount() {
		useAndOpenFolderIfNotSet();
		try {
			return folder.getDeletedMessageCount();
		} catch (final MessagingException msgexc) {
			throw new MailException(msgexc);
		}
	}

	// ---------------------------------------------------------------- receive builder

	/**
	 * Defines the process of received email in an fluent way.
	 */
	public ReceiverBuilder receive() {
		return new ReceiverBuilder(this);
	}

	// ---------------------------------------------------------------- receive emails

	/**
	 * Receives all emails. Messages are not modified. However, servers
	 * may set SEEN flag anyway, so we force messages to remain
	 * unseen.
	 *
	 * @return array of {@link ReceivedEmail}s.
	 */
	public ReceivedEmail[] receiveEmail() {
		return receiveMessages(null, null, null, false, null);
	}

	/**
	 * Receives all emails that matches given {@link EmailFilter}.
	 * Messages are not modified. However, servers may set SEEN flag anyway,
	 * so we force messages to remain unseen.
	 *
	 * @param filter {@link EmailFilter}
	 * @return array of {@link ReceivedEmail}s.
	 */
	public ReceivedEmail[] receiveEmail(final EmailFilter filter) {
		return receiveMessages(filter, null, null, false, null);
	}

	/**
	 * Receives all emails and mark all messages as 'seen' (ie 'read').
	 *
	 * @return array of {@link ReceivedEmail}s.
	 * @see #receiveEmailAndMarkSeen(EmailFilter)
	 */
	public ReceivedEmail[] receiveEmailAndMarkSeen() {
		return receiveEmailAndMarkSeen(null);
	}

	/**
	 * Receives all emails that matches given {@link EmailFilter}
	 * and mark them as 'seen' (ie 'read').
	 *
	 * @param filter {@link EmailFilter}
	 * @return array of {@link ReceivedEmail}s.
	 */
	public ReceivedEmail[] receiveEmailAndMarkSeen(final EmailFilter filter) {
		final Flags flagsToSet = new Flags();
		flagsToSet.add(Flags.Flag.SEEN);
		return receiveMessages(filter, flagsToSet, null, false, null);
	}

	/**
	 * Receives all emails and mark all messages as 'seen' and 'deleted'.
	 *
	 * @return array of {@link ReceivedEmail}s.
	 */
	public ReceivedEmail[] receiveEmailAndDelete() {
		return receiveEmailAndDelete(null);
	}

	/**
	 * Receives all emails that matches given {@link EmailFilter} and
	 * mark all messages as 'seen' and 'deleted'.
	 *
	 * @param filter {@link EmailFilter}
	 * @return array of {@link ReceivedEmail}s.
	 */
	public ReceivedEmail[] receiveEmailAndDelete(final EmailFilter filter) {
		final Flags flags = new Flags();
		flags.add(Flags.Flag.SEEN);
		flags.add(Flags.Flag.DELETED);
		return receiveMessages(filter, flags, null, false, null);
	}

	public ReceivedEmail[] receiveEnvelopes() {
		return receiveEnvelopes(null);
	}

	public ReceivedEmail[] receiveEnvelopes(final EmailFilter filter) {
		return receiveMessages(filter, null, null, true, null);
	}

	/**
	 * The main email receiving method.
	 */
	ReceivedEmail[] receiveMessages(
			final EmailFilter filter,
			final Flags flagsToSet,
			final Flags flagsToUnset,
			final boolean envelope,
			final Consumer<Message[]> processedMessageConsumer) {
		useAndOpenFolderIfNotSet();

		final Message[] messages;

		try {
			if (filter == null) {
				messages = folder.getMessages();
			} else {
				messages = folder.search(filter.getSearchTerm());
			}

			if (messages.length == 0) {
				return ReceivedEmail.EMPTY_ARRAY;
			}

			if (envelope) {
				final FetchProfile fetchProfile = new FetchProfile();

				fetchProfile.add(FetchProfile.Item.ENVELOPE);
				fetchProfile.add(FetchProfile.Item.FLAGS);

				folder.fetch(messages, fetchProfile);
			}

			// process messages

			final ReceivedEmail[] emails = new ReceivedEmail[messages.length];

			for (int i = 0; i < messages.length; i++) {
				final Message msg = messages[i];

				// we need to parse message BEFORE flags are set!
				emails[i] = new ReceivedEmail(msg, envelope, attachmentStorage);

				if (!EmailUtil.isEmptyFlags(flagsToSet)) {
					emails[i].flags(flagsToSet);
					msg.setFlags(flagsToSet, true);
				}

				if (!EmailUtil.isEmptyFlags(flagsToUnset)) {
					emails[i].flags().remove(flagsToUnset);
					msg.setFlags(flagsToUnset, false);
				}

				if (EmailUtil.isEmptyFlags(flagsToSet) && !emails[i].isSeen()) {
					msg.setFlag(Flags.Flag.SEEN, false);
				}
			}

			if (processedMessageConsumer != null) {
				processedMessageConsumer.accept(messages);
			}

			// if messages were marked to be deleted, we need to expunge the folder
			if (!EmailUtil.isEmptyFlags(flagsToSet)) {
				if (flagsToSet.contains(Flags.Flag.DELETED)) {
					folder.expunge();
				}
			}

			return emails;
		} catch (final MessagingException msgexc) {
			throw new MailException("Failed to fetch messages", msgexc);
		}
	}

	// ---------------------------------------------------------------- update

	/**
	 * Updates the email flags on the server.
	 */
	public void updateEmailFlags(final ReceivedEmail receivedEmail) {
		useAndOpenFolderIfNotSet();
		try {
			folder.setFlags(new int[] {receivedEmail.messageNumber()}, receivedEmail.flags(),true);
		} catch (MessagingException mex) {
			throw new MailException("Failed to fetch messages", mex);
		}
	}

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

	/**
	 * Closes folder if opened and expunge deleted messages.
	 */
	protected void closeFolderIfOpened(final Folder folder) {
		if (folder != null) {
			try {
				folder.close(true);
			} catch (final MessagingException ignore) {
			}
		}
	}

	@Override
	public void close() {
		closeFolderIfOpened(folder);
		folder = null;
		folderName = null;
		super.close();
	}

}


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

package jodd.db;

import jodd.util.CharUtil;
import jodd.util.StringUtil;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;

/**
 * SQL parameters parser that recognizes named and ordinal parameters.
 */
class DbQueryParser {

	public static final String SQL_SEPARATORS = " \n\r\f\t,()=<>&|+-=/*'^![]#~\\";

	boolean prepared;
	boolean callable;
	String sql;

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

	DbQueryParser(final String sql) {
		parseSql(sql);
	}

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

	private DbQueryNamedParameter rootNP;

	void storeNamedParameter(final String name, final int position) {
		DbQueryNamedParameter p = lookupNamedParameter(name);
		if (p == null) {
			p = new DbQueryNamedParameter(name);
			p.indices = new int[] {position};
			p.next = rootNP;
			rootNP = p;
		}
		else {
			p.add(position);
		}
	}

	/**
	 * Lookup for named parameter.
	 */
	DbQueryNamedParameter lookupNamedParameter(final String name) {
		DbQueryNamedParameter p = rootNP;
		while (p != null) {
			if (p.equalsName(name)) {
				return p;
			}
			p = p.next;
		}
		return null;
	}

	int[] getNamedParameterIndices(final String name) {
		final DbQueryNamedParameter p = lookupNamedParameter(name);
		if (p == null) {
			throw new DbSqlException("Named parameter not found: " + name + "\nQuery: " + sql);
		}
		return p.indices;
	}

	void forEachNamedParameter(final Consumer<DbQueryNamedParameter> consumer) {
		DbQueryNamedParameter p = rootNP;
		while (p != null) {
			consumer.accept(p);
			p = p.next;
		}
	}

	// ---------------------------------------------------------------- batch

	private Map<String, Integer> batchParams;

	private void saveBatchParameter(final String name, final int size) {
		if (batchParams == null) {
			batchParams = new HashMap<>();
		}
		batchParams.put(name, Integer.valueOf(size));
	}

	/**
	 * Returns the size of batch parameter. Returns 0
	 * if parameter does not exist.
	 */
	protected int getBatchParameterSize(final String name) {
		if (batchParams == null) {
			return 0;
		}
		Integer size = batchParams.get(name);
		if (size == null) {
			return 0;
		}
		return size.intValue();
	}

	// ---------------------------------------------------------------- parser

	void parseSql(final String sqlString) {
		rootNP = null;
		final int stringLength = sqlString.length();
		final StringBuilder pureSql = new StringBuilder(stringLength);
		boolean inQuote = false;
		int index = 0;
		int paramCount = 0;

		while (index < stringLength) {
			char c = sqlString.charAt(index);
			if (inQuote) {
				if (c == '\'') {
					inQuote = false;
				}
			}
			else if (c == '\'') {
				inQuote = true;
			}
			else if (c == ':' && index + 1 < stringLength
					&& sqlString.charAt(index + 1) == ':') {
				// don't treat '::foo' sequence as named parameter; skip this
				// chunk
				int right = StringUtil.indexOfChars(sqlString,
						SQL_SEPARATORS, index + 2);
				if (right < 0) {
					right = stringLength;
				}
				pureSql.append(sqlString.substring(index, right));
				index = right;
				continue;
			}
			else if (c == ':') {
				int right = StringUtil.indexOfChars(sqlString, SQL_SEPARATORS, index + 1);
				boolean batch = false;

				if (right < 0) {
					right = stringLength;
				} else {
					if (sqlString.charAt(right) == '!') {
						batch = true;
					}
				}

				String param = sqlString.substring(index + 1, right);

				if (!batch) {
					paramCount++;
					storeNamedParameter(param, paramCount);
					pureSql.append('?');
				}
				else {
					// read batch size
					right++;
					int numStart = right;

					while (right < stringLength) {
						if (!CharUtil.isDigit(sqlString.charAt(right))) {
							break;
						}
						right++;
					}

					String numberValue = sqlString.substring(numStart, right);
					int batchSize;
					try {
						batchSize = Integer.parseInt(numberValue);
					} catch (NumberFormatException nfex) {
						throw new DbSqlException("Batch size is not an integer: " + numberValue, nfex);
					}

					saveBatchParameter(param, batchSize);

					// create batch parameters
					for (int i = 1; i <= batchSize; i++) {
						if (i != 1) {
							pureSql.append(',');
						}
						paramCount++;
						storeNamedParameter(param + '.' + i, paramCount);
						pureSql.append('?');
					}
				}

				index = right;
				continue;
			}
			else if (c == '?') {		// either an ordinal or positional parameter
				if ((index < stringLength - 1) && (Character.isDigit(sqlString.charAt(index + 1)))) {   // positional parameter
					int right = StringUtil.indexOfChars(sqlString, SQL_SEPARATORS, index + 1);
					if (right < 0) {
						right = stringLength;
					}
					String param = sqlString.substring(index + 1, right);
					try {
						Integer.parseInt(param);
					} catch (NumberFormatException nfex) {
						throw new DbSqlException("Positional parameter is not an integer: " + param, nfex);
					}
					paramCount++;
					storeNamedParameter(param, paramCount);
					pureSql.append('?');
					index = right;
					continue;
				}
				paramCount++;		// ordinal param
			}
			pureSql.append(c);
			index++;
		}
		this.prepared = (paramCount != 0);
		this.sql = pureSql.toString();

		if (this.sql.startsWith("{")) {
			this.callable = 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.joy.madvoc;

import jodd.util.StringPool;
import jodd.util.StringUtil;
import jodd.vtor.Violation;
import jodd.vtor.Vtor;

import java.util.List;

/**
 * Abstract base application action.
 */
public abstract class AppAction {

	public static final String BACK = "#";
	public static final String OK = "ok";

	public static final String ALIAS_INDEX = "<index>";
	public static final String ALIAS_INDEX_NAME = "index";

	/**
	 * Creates alias.
	 */
	protected String alias(final String target) {
		return StringPool.LEFT_CHEV.concat(target).concat(StringPool.RIGHT_CHEV);
	}

	/**
	 * Creates alias from target class and target method name.
	 */
	protected String alias(final Class targetClass, final String targetMethodName) {
		return '<' + targetClass.getName() + '#' + targetMethodName + '>';
	}

	/**
	 * Creates alias from target object and target method name.
	 * If classname contains a '$' sign, everything will be stripped
	 * after it (to get the real name, if action class is proxified).
	 */
	protected String alias(final Object target, final String targetMethodName) {
		String targetClassName = target.getClass().getName();
		targetClassName = StringUtil.cutToIndexOf(targetClassName, '$');
		return '<' + targetClassName + '#' + targetMethodName + '>';
	}

	// ---------------------------------------------------------------- validation

	protected Vtor vtor;

	/**
	 * Returns validation violations or null if validation was successful.
	 */
	public List<Violation> violations() {
		if (vtor == null) {
			return null;
		}
		return vtor.getViolations();
	}

	@SuppressWarnings({"NullArgumentToVariableArgMethod"})
	protected boolean validateAction() {
		return validateAction(null);
	}

	/**
	 * Validates action. Profiles are reset after the invocation.
	 * @return true if validation is successful, otherwise returns false
	 */
	protected boolean validateAction(final String... profiles) {
		prepareValidator();
		vtor.useProfiles(profiles);
		vtor.validate(this);
		vtor.resetProfiles();
		List<Violation> violations = vtor.getViolations();
		return violations == null;
	}

	/**
	 * Adds action violation.
	 */
	protected void addViolation(final String name, final Object invalidValue) {
		prepareValidator();
		vtor.addViolation(new Violation(name, this, invalidValue));
	}

	/**
	 * Adds action violation.
	 */
	protected void addViolation(final String name) {
		addViolation(name, null);
	}

	protected void prepareValidator() {
		if (vtor == null) {
			vtor = new Vtor();
		}
	}

}


// 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.util.CharSequenceUtil;

/**
 * Rules for void tags.
 */
public class HtmlVoidRules {

	/**
	 * Default void tags.
	 * http://dev.w3.org/html5/spec/Overview.html#void-elements
	 */
	private static final String[] HTML5_VOID_TAGS = {
			"area", "base", "br", "col", "embed", "hr", "img", "input",
			"keygen", "link", "menuitem", "meta", "param", "source",
			"track", "wbr"};

	/**
	 * Returns true if tag name is a void tag.
	 */
	public boolean isVoidTag(final CharSequence tagName) {
		for (String html5VoidTag : HTML5_VOID_TAGS) {
			if (CharSequenceUtil.equalsToLowercase(tagName, html5VoidTag)) {
				return true;
			}
		}
		return false;
	}

}


// 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.io.FileNameUtil;
import jodd.madvoc.MadvocException;
import jodd.madvoc.meta.RenderWith;
import jodd.util.StringPool;
import jodd.net.MimeTypes;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

/**
 * Raw data to download.
 */
@RenderWith(RawActionResult.class)
public class RawData {

	protected final InputStream inputStream;
	protected final int length;

	protected String downloadFileName;
	protected String mimeType = MimeTypes.MIME_APPLICATION_OCTET_STREAM;

	public static RawData of(final byte[] bytes) {
		return new RawData(new ByteArrayInputStream(bytes), bytes.length);
	}

	public static RawData of(final File file) {
		return new RawData(createFileInputStream(file), (int) file.length()).downloadableAs(file.getName());
	}

	public RawData(final InputStream inputStream, final int length) {
		this.inputStream = inputStream;
		this.length = length;
	}

	/**
	 * Defines mime type by providing real mime type or just extension!
	 */
	public RawData as(final String mimeOrExtension) {
		if (mimeOrExtension.contains(StringPool.SLASH)) {
			this.mimeType = mimeOrExtension;
		}
		else {
			this.mimeType = MimeTypes.getMimeType(mimeOrExtension);
		}
		return this;
	}

	public RawData asHtml() {
		this.mimeType = MimeTypes.MIME_TEXT_HTML;
		return this;
	}
	public RawData asText() {
		this.mimeType = MimeTypes.MIME_TEXT_PLAIN;
		return this;
	}

	/**
	 * Defines download file name and mime type from the name extension.
	 */
	public RawData downloadableAs(final String downloadFileName) {
		this.downloadFileName = downloadFileName;
		this.mimeType = MimeTypes.getMimeType(FileNameUtil.getExtension(downloadFileName));
		return this;
	}

	// ---------------------------------------------------------------- getter

	public InputStream contentInputStream() {
		return inputStream;
	}

	public String mimeType() {
		return mimeType;
	}

	public String downloadFileName() {
		return downloadFileName;
	}

	public int contentLength() {
		return length;
	}

	private static FileInputStream createFileInputStream(final File file) {
		try {
			return new FileInputStream(file);
		} catch (FileNotFoundException fis) {
			throw new MadvocException(fis);
		}
	}

}


// 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.asm7.ClassReader;
import jodd.asm7.ClassWriter;
import jodd.bridge.DefineClass;
import jodd.io.FileUtil;
import jodd.io.StreamUtil;
import jodd.log.Logger;
import jodd.log.LoggerFactory;
import jodd.proxetta.asm.TargetClassInfoReader;
import jodd.proxetta.asm.WorkData;
import jodd.util.ClassLoaderUtil;
import jodd.util.ClassUtil;
import jodd.util.StringUtil;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

/**
 * Proxetta builder. While {@link Proxetta} only holds aspects and
 * configuration, ProxettaBuilder deals with the
 * actually building proxies and wrappers over provided target.
 */
public abstract class ProxettaFactory<T extends ProxettaFactory, P extends Proxetta> {

	Logger log = LoggerFactory.getLogger(ProxettaFactory.class);

	protected final P proxetta;

	/**
	 * Creates new builder.
	 */
	protected ProxettaFactory(final P proxetta) {
		this.proxetta = proxetta;
	}

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

	// ---------------------------------------------------------------- IN

	/**
	 * Main target source.
	 */
	private InputStream targetInputStream;

	/**
	 * Target class, when available.
	 */
	private Class targetClass;

	/**
	 * Target class name, when available.
	 */
	private String targetClassName;

	/**
	 * Requested proxy class name (or class name template).
	 */
	protected String requestedProxyClassName;

	/**
	 * Sets requested proxy class name.
	 */
	public T setTargetProxyClassName(final String targetProxyClassName) {
		this.requestedProxyClassName = targetProxyClassName;
		return _this();
	}

	// ---------------------------------------------------------------- IN targets

	/**
	 * Defines class input stream as a target.
	 */
	protected T setTarget(final InputStream target) {
		assertTargetIsNotDefined();

		targetInputStream = target;
		targetClass = null;
		targetClassName = null;

		return _this();
	}

	/**
	 * Defines class name as a target.
	 * Class will not be loaded by classloader!
	 */
	protected T setTarget(final String targetName) {
		assertTargetIsNotDefined();

		try {
			targetInputStream = ClassLoaderUtil.getClassAsStream(targetName);
			if (targetInputStream == null) {
				throw new ProxettaException("Target class not found: " + targetName);
			}
			targetClassName = targetName;
			targetClass = null;
		}
		catch (IOException ioex) {
			StreamUtil.close(targetInputStream);
			throw new ProxettaException("Unable to get stream class name: " + targetName, ioex);
		}
		return _this();
	}

	/**
	 * Defines class as a target.
	 */
	public T setTarget(final Class target) {
		assertTargetIsNotDefined();

		try {
			targetInputStream = ClassLoaderUtil.getClassAsStream(target);
			if (targetInputStream == null) {
				throw new ProxettaException("Target class not found: " + target.getName());
			}
			targetClass = target;
			targetClassName = target.getName();
		}
		catch (IOException ioex) {
			StreamUtil.close(targetInputStream);
			throw new ProxettaException("Unable to stream class: " + target.getName(), ioex);
		}
		return _this();
	}

	/**
	 * Checks if target is not defined yet.
	 */
	private void assertTargetIsNotDefined() {
		if (targetInputStream != null) {
			throw new ProxettaException("Target already defined");
		}

	}

	// ---------------------------------------------------------------- IN naming

	/**
	 * Number appended to proxy class name, incremented on each use to make classnames unique
	 * in the system (e.g. classloader).
	 *
	 * @see Proxetta#setVariableClassName(boolean)
 	 */
	protected static int suffixCounter;

	/**
	 * Returns new suffix or null if suffix is not in use.
	 */
	protected String resolveClassNameSuffix() {
		String classNameSuffix = proxetta.getClassNameSuffix();

		if (classNameSuffix == null) {
			return null;
		}

		if (!proxetta.isVariableClassName()) {
			return classNameSuffix;
		}

		suffixCounter++;
		return classNameSuffix + suffixCounter;
	}

	// ---------------------------------------------------------------- PROCESS

	/**
	 * Creates custom class builder and process the target class with it.
	 */
	protected abstract WorkData process(ClassReader cr, TargetClassInfoReader targetClassInfoReader);

	// ---------------------------------------------------------------- ACCEPT

	protected ClassWriter destClassWriter;			// destination class writer
	protected boolean proxyApplied;
	protected String proxyClassName;

	/**
	 * Reads the target and creates destination class.
	 */
	protected void process() {
		if (targetInputStream == null) {
			throw new ProxettaException("Target missing: " + targetClassName);
		}
		// create class reader
		final ClassReader classReader;
		try {
			classReader = new ClassReader(targetInputStream);
		} catch (IOException ioex) {
			throw new ProxettaException("Error reading class input stream", ioex);
		}

		// reads information
		final TargetClassInfoReader targetClassInfoReader = new TargetClassInfoReader(proxetta.getClassLoader());
		classReader.accept(targetClassInfoReader, 0);

		this.destClassWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);

		// create proxy
		if (log.isDebugEnabled()) {
			log.debug("processing: " + classReader.getClassName());
		}
		WorkData wd = process(classReader, targetClassInfoReader);

		// store important data
		proxyApplied = wd.proxyApplied;
		proxyClassName = wd.thisReference.replace('/', '.');
	}

	/**
	 * Returns byte array of created class.
	 */
	public byte[] create() {
		process();

		byte[] result = toByteArray();

		dumpClassInDebugFolder(result);

		if ((!proxetta.isForced()) && (!isProxyApplied())) {
			if (log.isDebugEnabled()) {
				log.debug("Proxy not applied: " + StringUtil.toSafeString(targetClassName));
			}
			return null;
		}

		if (log.isDebugEnabled()) {
			log.debug("Proxy created " + StringUtil.toSafeString(targetClassName));
		}

		return result;
	}

	/**
	 * Defines class.
	 */
	public Class define() {
		process();

		if ((!proxetta.isForced()) && (!isProxyApplied())) {
			if (log.isDebugEnabled()) {
				log.debug("Proxy not applied: " + StringUtil.toSafeString(targetClassName));
			}

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

			if (targetClassName != null) {
				try {
					return ClassLoaderUtil.loadClass(targetClassName);
				} catch (ClassNotFoundException cnfex) {
					throw new ProxettaException(cnfex);
				}
			}
		}

		if (log.isDebugEnabled()) {
			log.debug("Proxy created: " + StringUtil.toSafeString(targetClassName));
		}

		try {
			ClassLoader classLoader = proxetta.getClassLoader();

			if (classLoader == null) {
				classLoader = ClassLoaderUtil.getDefaultClassLoader();

				if ((classLoader == null) && (targetClass != null)) {
					classLoader = targetClass.getClassLoader();
				}
			}

			final byte[] bytes = toByteArray();

			dumpClassInDebugFolder(bytes);

			return DefineClass.of(getProxyClassName(), bytes, classLoader);
		} catch (Exception ex) {
			throw new ProxettaException("Class definition failed", ex);
		}
	}

	/**
	 * Creates new instance of created class.
	 * Assumes default no-arg constructor.
	 */
	public Object newInstance() {
		Class type = define();
		try {
			return ClassUtil.newInstance(type);
		} catch (Exception ex) {
			throw new ProxettaException("Invalid Proxetta class", ex);
		}
	}

	// ---------------------------------------------------------------- debug

	/**
	 * Writes created class content to output folder for debugging purposes.
	 */
	protected void dumpClassInDebugFolder(final byte[] bytes) {
		File debugFolder = proxetta.getDebugFolder();
		if (debugFolder == null) {
			return;
		}

		if (!debugFolder.exists() || !debugFolder.isDirectory()) {
			log.warn("Invalid debug folder: " + debugFolder);
		}

		String fileName = proxyClassName;
		if (fileName == null) {
			fileName = "proxetta-" + System.currentTimeMillis();
		}

		fileName += ".class";

		File file = new File(debugFolder, fileName);
		try {
			FileUtil.writeBytes(file, bytes);
		} catch (IOException ioex) {
			log.warn("Error writing class as " + file, ioex);
		}
	}

	// ---------------------------------------------------------------- OUT

	/**
	 * Checks if proxy is created and throws an exception if not.
	 */
	protected void assertProxyIsCreated() {
		if (destClassWriter == null) {
			throw new ProxettaException("Target not accepted yet!");
		}
	}

	/**
	 * Returns raw bytecode.
	 */
	protected byte[] toByteArray() {
		assertProxyIsCreated();
		return destClassWriter.toByteArray();
	}

	/**
	 * Returns true if at least one method was wrapped.
	 */
	public boolean isProxyApplied() {
		assertProxyIsCreated();
		return proxyApplied;
	}

	/**
	 * Returns proxy class name.
	 */
	public String getProxyClassName() {
		assertProxyIsCreated();
		return proxyClassName;
	}

}


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

package jodd.mail;

import jodd.util.ArraysUtil;
import jodd.net.MimeTypes;

import javax.activation.DataSource;
import javax.mail.Address;
import javax.mail.Header;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Common stuff for both {@link Email} and {@link ReceivedEmail}.
 */
public abstract class CommonEmail<T extends CommonEmail<T>> {

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

	public static final String X_PRIORITY = "X-Priority";

	public static final int PRIORITY_HIGHEST = 1;
	public static final int PRIORITY_HIGH = 2;
	public static final int PRIORITY_NORMAL = 3;
	public static final int PRIORITY_LOW = 4;
	public static final int PRIORITY_LOWEST = 5;

	/**
	 * Clones the email with all its necessary data.
	 *
	 * @return new object of type T
	 */
	@Override
	public abstract T clone();

	// ---------------------------------------------------------------- from

	/**
	 * FROM address.
	 */
	private EmailAddress from;

	/**
	 * Sets the FROM address.
	 *
	 * @param from {@link EmailAddress}.
	 * @return this
	 */
	public T from(final EmailAddress from) {
		this.from = from;
		return _this();
	}

	/**
	 * Sets the FROM address from {@link Address}.
	 *
	 * @param from {@link Address}
	 * @return this
	 * @see #from(EmailAddress)
	 */
	public T from(final Address from) {
		return from(EmailAddress.of(from));
	}

	/**
	 * Sets the FROM address.
	 *
	 * @param from Address may be specified with personal name like this: {@code Jenny Doe <email@foo.com>}.
	 * @return this
	 * @see #from(EmailAddress)
	 */
	public T from(final String from) {
		return from(EmailAddress.of(from));
	}

	/**
	 * Sets the FROM address by providing personal name and address.
	 *
	 * @param personalName personal name.
	 * @param from         email address.
	 * @return this
	 * @see #from(EmailAddress)
	 */
	public T from(final String personalName, final String from) {
		return from(new EmailAddress(personalName, from));
	}

	/**
	 * Returns FROM {@link EmailAddress}.
	 */
	public EmailAddress from() {
		return from;
	}

	// ---------------------------------------------------------------- to

	/**
	 * TO address.
	 */
	private EmailAddress[] to = EmailAddress.EMPTY_ARRAY;

	/**
	 * Appends TO address.
	 *
	 * @param to {@link EmailAddress} to add.
	 * @return this
	 */
	public T to(final EmailAddress to) {
		this.to = ArraysUtil.append(this.to, to);
		return _this();
	}

	/**
	 * Appends TO address.
	 *
	 * @param to Address may be specified with personal name like this: {@code Jenny Doe <email@foo.com>}.
	 * @return this
	 * @see #to(EmailAddress)
	 */
	public T to(final String to) {
		return to(EmailAddress.of(to));
	}

	/**
	 * Appends TO address by personal name and email address.
	 *
	 * @param personalName personal name.
	 * @param to           email address.
	 * @return this
	 * @see #to(EmailAddress)
	 */
	public T to(final String personalName, final String to) {
		return to(new EmailAddress(personalName, to));
	}

	/**
	 * Appends TO address from {@code Address}.
	 *
	 * @param to {@link Address} to add.
	 * @return this
	 * @see #to(EmailAddress)
	 */
	public T to(final Address to) {
		return to(EmailAddress.of(to));
	}

	/**
	 * Appends one or more TO address.
	 *
	 * @param tos Address may be specified with personal name like this: {@code Jenny Doe <email@foo.com>}.
	 * @return this
	 * @see #to(EmailAddress...)
	 */
	public T to(final String... tos) {
		return to(EmailAddress.of(tos));
	}

	/**
	 * Appends one or more TO addresses.
	 *
	 * @param tos array of {@link Address}s to set.
	 * @return this
	 * @see #to(EmailAddress...)
	 */
	public T to(final Address... tos) {
		return to(EmailAddress.of(tos));
	}

	/**
	 * Appends TO addresses.
	 *
	 * @param tos vararg of {@link EmailAddress}es to set.
	 * @return this
	 */
	public T to(final EmailAddress... tos) {
		this.to = valueOrEmptyArray(tos);
		return _this();
	}

	/**
	 * Returns TO addresses.
	 */
	public EmailAddress[] to() {
		return to;
	}

	/**
	 * Resets TO addresses.
	 */
	public T resetTo() {
		this.to = EmailAddress.EMPTY_ARRAY;
		return _this();
	}

	// ---------------------------------------------------------------- reply-to

	private EmailAddress[] replyTo = EmailAddress.EMPTY_ARRAY;

	/**
	 * Appends REPLY-TO address.
	 *
	 * @param replyTo {@link EmailAddress} to add.
	 * @return this
	 */
	public T replyTo(final EmailAddress replyTo) {
		this.replyTo = ArraysUtil.append(this.replyTo, replyTo);
		return _this();
	}

	/**
	 * Appends REPLY-TO address.
	 *
	 * @param replyTo Address may be specified with personal name like this: {@code Jenny Doe <email@foo.com>}.
	 * @return this
	 * @see #replyTo(EmailAddress)
	 */
	public T replyTo(final String replyTo) {
		return replyTo(EmailAddress.of(replyTo));
	}

	/**
	 * Appends REPLY-TO address.
	 *
	 * @param personalName personal name.
	 * @param replyTo      email address.
	 * @return this
	 * @see #replyTo(EmailAddress)
	 */
	public T replyTo(final String personalName, final String replyTo) {
		return replyTo(new EmailAddress(personalName, replyTo));
	}

	/**
	 * Appends REPLY-TO address.
	 *
	 * @param replyTo {@link Address} to add.
	 * @return this
	 * @see #replyTo(EmailAddress)
	 */
	public T replyTo(final Address replyTo) {
		return replyTo(EmailAddress.of(replyTo));
	}

	/**
	 * Appends one or more REPLY-TO address.
	 *
	 * @param replyTos array of {@link EmailAddress}es to set.
	 * @return this
	 * @see #replyTo(EmailAddress...)
	 */
	public T replyTo(final String... replyTos) {
		return replyTo(EmailAddress.of(replyTos));
	}

	/**
	 * Appeds one or more REPLY-TO address.
	 *
	 * @param replyTos array of {@link Address}es to set.
	 * @return this
	 * @see #replyTo(EmailAddress...)
	 */
	public T replyTo(final Address... replyTos) {
		return replyTo(EmailAddress.of(replyTos));
	}

	/**
	 * Appends REPLY-TO addresses.
	 *
	 * @param replyTo vararg of {@link EmailAddress}es to set.
	 * @return this
	 */
	public T replyTo(final EmailAddress... replyTo) {
		this.replyTo = ArraysUtil.join(this.replyTo, valueOrEmptyArray(replyTo));
		return _this();
	}

	/**
	 * Returns REPLY-TO addresses.
	 */
	public EmailAddress[] replyTo() {
		return replyTo;
	}

	/**
	 * Resets all REPLY-To addresses.
	 */
	public T resetReplyTo() {
		this.replyTo = EmailAddress.EMPTY_ARRAY;
		return _this();
	}

	// ---------------------------------------------------------------- cc

	/**
	 * CC address.
	 */
	private EmailAddress[] cc = EmailAddress.EMPTY_ARRAY;

	/**
	 * Appends CC address.
	 *
	 * @param to {@link EmailAddress} to add.
	 * @return this
	 */
	public T cc(final EmailAddress to) {
		this.cc = ArraysUtil.append(this.cc, to);
		return _this();
	}

	/**
	 * Appends CC address.
	 *
	 * @param cc Address may be specified with personal name like this: {@code Jenny Doe <email@foo.com>}.
	 * @return this
	 * @see #cc(EmailAddress)
	 */
	public T cc(final String cc) {
		return cc(EmailAddress.of(cc));
	}

	/**
	 * Appends CC address.
	 *
	 * @param personalName personal name.
	 * @param cc           email address.
	 * @return this
	 * @see #cc(EmailAddress)
	 */
	public T cc(final String personalName, final String cc) {
		return cc(new EmailAddress(personalName, cc));
	}

	/**
	 * Appends CC address.
	 *
	 * @param cc {@link Address} to add.
	 * @return this
	 * @see #cc(EmailAddress)
	 */
	public T cc(final Address cc) {
		return cc(EmailAddress.of(cc));
	}

	/**
	 * Sets one or more CC address.
	 *
	 * @param ccs array of {@link String}s to set.
	 * @return this
	 * @see #cc(EmailAddress...)
	 */
	public T cc(final String... ccs) {
		return cc(EmailAddress.of(ccs));
	}

	/**
	 * Sets one or more CC address.
	 *
	 * @param ccs array of {@link Address}s to set.
	 * @return this
	 * @see #cc(EmailAddress...)
	 */
	public T cc(final Address... ccs) {
		return cc(EmailAddress.of(ccs));
	}

	/**
	 * Appends CC addresses.
	 *
	 * @param ccs vararg of {@link EmailAddress}es to set.
	 * @return this
	 */
	public T cc(final EmailAddress... ccs) {
		this.cc = ArraysUtil.join(this.cc, valueOrEmptyArray(ccs));
		return _this();
	}

	/**
	 * Returns CC addresses.
	 */
	public EmailAddress[] cc() {
		return cc;
	}

	/**
	 * Resets all CC addresses.
	 */
	public T resetCc() {
		this.cc = EmailAddress.EMPTY_ARRAY;
		return _this();
	}

	// ---------------------------------------------------------------- subject

	/**
	 * Message subject.
	 */
	private String subject;

	/**
	 * Message subject encoding.
	 */
	private String subjectEncoding;

	/**
	 * Sets message subject.
	 *
	 * @param subject The message subject to set.
	 * @return this
	 */
	public T subject(final String subject) {
		this.subject = subject;
		return _this();
	}

	/**
	 * Sets message subject with specified encoding to override default platform encoding.
	 * If the subject contains non US-ASCII characters, it will be encoded using the specified charset.
	 * If the subject contains only US-ASCII characters, no encoding is done and it is used as-is.
	 * The application must ensure that the subject does not contain any line breaks.
	 * See {@link javax.mail.internet.MimeMessage#setSubject(String, String)}.
	 *
	 * @param subject  The message subject
	 * @param encoding The encoding for the message subject.
	 * @return this
	 */
	public T subject(final String subject, final String encoding) {
		subject(subject);
		this.subjectEncoding = encoding;
		return _this();
	}

	/**
	 * Returns message subject.
	 *
	 * @return message subject.
	 */
	public String subject() {
		return this.subject;
	}

	/**
	 * Returns the message subject encoding.
	 *
	 * @return the message subject encoding.
	 */
	public String subjectEncoding() {
		return this.subjectEncoding;
	}

	// ---------------------------------------------------------------- message

	/**
	 * All messages.
	 */
	private final List<EmailMessage> messages = new ArrayList<>();

	/**
	 * Returns all messages.
	 */
	public List<EmailMessage> messages() {
		return messages;
	}

	/**
	 * Adds multiple messages.
	 *
	 * @param msgsToAdd {@link List} of {@link EmailMessage}s to add.
	 * @return this
	 */
	public T message(final List<EmailMessage> msgsToAdd) {
		messages.addAll(msgsToAdd);
		return _this();
	}

	/**
	 * Adds an {@link EmailMessage}.
	 *
	 * @param msgToAdd {@link EmailMessage} to add.
	 * @return this
	 */
	public T message(final EmailMessage msgToAdd) {
		messages.add(msgToAdd);
		return _this();
	}

	/**
	 * Adds a {@link EmailMessage}.
	 *
	 * @param text     The text to add as a {@link String}.
	 * @param mimeType The MIME type as a {@link String}.
	 * @param encoding The encoding as a {@link String}.
	 * @return this
	 * @see #message(EmailMessage)
	 */
	public T message(final String text, final String mimeType, final String encoding) {
		return message(new EmailMessage(text, mimeType, encoding));
	}

	/**
	 * Adds a {@link EmailMessage}.
	 *
	 * @param text     The text to add as a {@link String}.
	 * @param mimeType The MIME type as a {@link String}.
	 * @return this
	 * @see #message(EmailMessage)
	 */
	public T message(final String text, final String mimeType) {
		return message(new EmailMessage(text, mimeType));
	}

	/**
	 * Adds plain message text.
	 *
	 * @param text The text to add as a {@link String}.
	 * @return this
	 * @see #message(String, String)
	 */
	public T textMessage(final String text) {
		return message(text, MimeTypes.MIME_TEXT_PLAIN);
	}

	/**
	 * Adds plain message text.
	 *
	 * @param text     The text to add as a {@link String}.
	 * @param encoding The encoding as a {@link String}.
	 * @return this
	 * @see #message(EmailMessage)
	 */
	public T textMessage(final String text, final String encoding) {
		return message(new EmailMessage(text, MimeTypes.MIME_TEXT_PLAIN, encoding));
	}

	/**
	 * Adds HTML message.
	 *
	 * @param html The HTML to add as a {@link String}.
	 * @return this
	 * @see #message(EmailMessage)
	 */
	public T htmlMessage(final String html) {
		return message(new EmailMessage(html, MimeTypes.MIME_TEXT_HTML));
	}

	/**
	 * Adds HTML message.
	 *
	 * @param html     The HTML to add as a {@link String}.
	 * @param encoding The encoding as a {@link String}.
	 * @return this
	 * @see #message(EmailMessage)
	 */
	public T htmlMessage(final String html, final String encoding) {
		return message(new EmailMessage(html, MimeTypes.MIME_TEXT_HTML, encoding));
	}

	// ---------------------------------------------------------------- headers

	/**
	 * All headers.
	 */
	private final Map<String, String> headers = new HashMap<>();

	/**
	 * Returns all headers as a {@link HashMap}.
	 *
	 * @return all headers in a {@link HashMap}
	 */
	protected Map<String, String> headers() {
		return headers;
	}

	/**
	 * Sets header value.
	 *
	 * @param name  The name of the header.
	 * @param value The value of the header.
	 * @return this
	 */
	public T header(final String name, final String value) {
		headers.put(name, value);
		return _this();
	}

	/**
	 * Sets headers.
	 *
	 * @param headersToSet Headers to set.
	 * @return this
	 */
	public T headers(final Map<String, String> headersToSet) {
		headers.putAll(headersToSet);
		return _this();
	}

	/**
	 * Sets headers.
	 *
	 * @param headersToSet Headers to set.
	 * @return this
	 * @see #header(String, String)
	 */
	public T headers(final Enumeration<Header> headersToSet) {
		while (headersToSet.hasMoreElements()) {
			final Header header = headersToSet.nextElement();
			header(header.getName(), header.getValue());
		}
		return _this();
	}

	/**
	 * Returns the value of a header.
	 *
	 * @param name The name of the header.
	 * @return The value of the header.
	 */
	public String header(final String name) {
		return headers.get(name);
	}

	/**
	 * Sets email priority.
	 *
	 * @param priority - Values of 1 through 5 are acceptable, with 1 being the highest priority, 3 = normal
	 *                 and 5 = lowest priority.
	 */
	public T priority(final int priority) {
		header(X_PRIORITY, String.valueOf(priority));
		return _this();
	}

	/**
	 * Returns emails priority (1 - 5) or <code>-1</code> if priority not available.
	 *
	 * @see #priority(int)
	 */
	public int priority() {
		try {
			return Integer.parseInt(headers.get(X_PRIORITY));
		} catch (final NumberFormatException ignore) {
			return -1;
		}
	}

	// ---------------------------------------------------------------- attachments

	private final List<EmailAttachment<? extends DataSource>> attachments = new ArrayList<>();

	/**
	 * Returns the list of all {@link EmailAttachment}s.
	 *
	 * @return List of {@link EmailAttachment}s. Returns empty list if no attachment is available.
	 */
	public List<EmailAttachment<? extends DataSource>> attachments() {
		return attachments;
	}

	/**
	 * Adds {@link EmailAttachment}s.
	 *
	 * @param attachments {@link List} of {@link EmailAttachment}s to add.
	 * @return this
	 */
	protected T storeAttachments(final List<EmailAttachment<? extends DataSource>> attachments) {
		this.attachments.addAll(attachments);
		return _this();
	}

	/**
	 * Adds {@link EmailAttachment}.
	 *
	 * @param attachment {@link EmailAttachment} to add.
	 * @return this
	 */
	protected T storeAttachment(final EmailAttachment<? extends DataSource> attachment) {
		this.attachments.add(attachment);
		return _this();
	}

	/**
	 * Adds {@link EmailAttachment}s.
	 *
	 * @param attachments {@link List} of {@link EmailAttachment}s to add.
	 * @return this
	 */
	public T attachments(final List<EmailAttachment<? extends DataSource>> attachments) {
		for (final EmailAttachment<?> attachment : attachments) {
			attachment(attachment);
		}
		return _this();
	}

	/**
	 * Adds {@link EmailAttachment}. Content ID will be set to {@code null}.
	 *
	 * @param attachment {@link EmailAttachment} to add.
	 * @return this
	 */
	public T attachment(final EmailAttachment<? extends DataSource> attachment) {
		attachment.setContentId(null);
		return storeAttachment(attachment);
	}

	/**
	 * @see #attachment(EmailAttachment)
	 */
	public T attachment(final EmailAttachmentBuilder builder) {
		return attachment(builder.buildByteArrayDataSource());
	}

	/**
	 * Attaches the embedded attachment: Content ID will be set if missing from attachment's file name.
	 *
	 * @param builder {@link EmailAttachmentBuilder}
	 * @return this
	 * @see #embeddedAttachment(EmailAttachment)
	 */
	public T embeddedAttachment(final EmailAttachmentBuilder builder) {
		builder.setContentIdFromNameIfMissing();

		// https://github.com/oblac/jodd/issues/546
		// https://github.com/oblac/jodd/issues/404#issuecomment-297011351
		// content disposition will be set to "inline"
		builder.inline(true);

		return embeddedAttachment(builder.buildByteArrayDataSource());
	}

	/**
	 * Embed {@link EmailAttachment} to last message. No header is changed.
	 *
	 * @param attachment {@link EmailAttachment}
	 * @return this
	 * @see #storeAttachment(EmailAttachment)
	 */
	public T embeddedAttachment(final EmailAttachment<? extends DataSource> attachment) {
		storeAttachment(attachment);

		final List<EmailMessage> messages = messages();
		final int size = messages.size();
		if (size > 1) {
			// Add to last message
			final int lastMessagePos = size - 1;
			final EmailMessage lastMessage = messages.get(lastMessagePos);
			attachment.setEmbeddedMessage(lastMessage);
		}

		return _this();
	}

	// ---------------------------------------------------------------- date

	/**
	 * Email's sent date.
	 */
	private Date sentDate;

	/**
	 * Sets email's sent date.
	 *
	 * @param date - Email's sent date. If {@code null}, then date will be set during the process of sending.
	 * @return this
	 */
	public T sentDate(final Date date) {
		sentDate = date;
		return _this();
	}

	/**
	 * Returns email's sent date. If return value is {@code null}, then date
	 * will be set during the process of sending.
	 *
	 * @return email's sent date or {@code null} if it will be set later.
	 */
	public Date sentDate() {
		return sentDate;
	}

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

	@Override
	public String toString() {
		return "Email{'" + from() + "\', subject='" + subject() + "\'}";
	}

	// ---------------------------------------------------------------- helper

	protected EmailAddress[] valueOrEmptyArray(EmailAddress[] arr) {
		if (arr == null) {
			arr = EmailAddress.EMPTY_ARRAY;
		}
		return arr;
	}
}


// 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.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Objects;

public class SystemUtil {

	/**
	 * Returns system property or {@code null} if not set.
	 */
	public static String get(final String name) {
		return get(name, null);
	}

	/**
	 * Returns system property. If key is not available, returns the default value.
	 */
	public static String get(final String name, final String defaultValue) {
		Objects.requireNonNull(name);

		String value = null;
		try {
			if (System.getSecurityManager() == null) {
				value = System.getProperty(name);
			} else {
				value = AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty(name));
			}
		} catch (Exception ignore) {
		}

		if (value == null) {
			return defaultValue;
		}

		return value;
	}

	/**
	 * Returns system property as boolean.
	 */
	public static boolean getBoolean(final String name, final boolean defaultValue) {
		String value = get(name);
		if (value == null) {
			return defaultValue;
		}

		value = value.trim().toLowerCase();

		switch (value) {
			case "true" :
			case "yes"  :
			case "1"    :
			case "on"   :
				return true;
			case "false":
			case "no"   :
			case "0"    :
			case "off"  :
				return false;
			default:
				return defaultValue;
		}
	}

	/**
	 * Returns system property as an int.
	 */
	public static long getInt(final String name, final int defaultValue) {
		String value = get(name);
		if (value == null) {
			return defaultValue;
		}

		value = value.trim().toLowerCase();
		try {
			return Integer.parseInt(value);
		}
		catch (NumberFormatException nfex) {
			return defaultValue;
		}
	}

	/**
	 * Returns system property as a long.
	 */
	public static long getLong(final String name, final long defaultValue) {
		String value = get(name);
		if (value == null) {
			return defaultValue;
		}

		value = value.trim().toLowerCase();
		try {
			return Long.parseLong(value);
		}
		catch (NumberFormatException nfex) {
			return defaultValue;
		}
	}

	// ---------------------------------------------------------------- infos

	private static final SystemInfo systemInfo = new SystemInfo();

	/**
	 * Returns system information.
	 */
	public static SystemInfo info() {
		return systemInfo;
	}

	/**
	 * Dump all information to the console.
	 */
	public static void printoutInfo() {
		System.out.println(systemInfo.toString());
	}

	public static void main(String[] args) {
		printoutInfo();
	}
}


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

import jodd.lagarto.dom.DOMBuilder;
import jodd.lagarto.dom.Document;
import jodd.lagarto.dom.LagartoDOMBuilder;
import jodd.lagarto.dom.Node;
import jodd.lagarto.dom.NodeSelector;
import jodd.lagarto.dom.Text;
import jodd.util.ArraysUtil;
import jodd.util.Format;
import jodd.util.StringPool;
import jodd.util.StringUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * Jerry is JQuery in Java.
 */
@SuppressWarnings("MethodNamesDifferingOnlyByCase")
public class Jerry implements Iterable<Jerry> {

	@SuppressWarnings("CloneableClassWithoutClone")
	private static class NodeList extends ArrayList<Node> {

		private NodeList(final int initialCapacity) {
			super(initialCapacity);
		}

		private NodeList() {
		}

		@Override
		public boolean add(final Node o) {
			for (Node node : this) {
				if (node == o) {
					return false;
				}
			}
			return super.add(o);
		}
	}

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

	/**
	 * Parses input sequence and creates new Jerry.
	 */
	public static Jerry jerry(final char[] content) {
		return jerry().parse(content);
	}

	/**
	 * Parses input content and creates new Jerry.
	 */
	public static Jerry jerry(final String content) {
		return jerry().parse(content);
	}

	// ---------------------------------------------------------------- 2-steps init

	/**
	 * Content parser and Jerry factory.
	 */
	public static class JerryParser {

		protected final DOMBuilder domBuilder;

		public JerryParser() {
			this.domBuilder = new LagartoDOMBuilder();
		}
		public JerryParser(final DOMBuilder domBuilder) {
			this.domBuilder = domBuilder;
		}

		/**
		 * Returns {@link DOMBuilder} for additional configuration.
		 */
		public DOMBuilder getDOMBuilder() {
			return domBuilder;
		}

		/**
		 * Invokes parsing on {@link DOMBuilder}.
		 */
		public Jerry parse(final char[] content) {
			Document doc = domBuilder.parse(content);
			return new Jerry(domBuilder, doc);
		}

		/**
		 * Invokes parsing on {@link DOMBuilder}.
		 */
		public Jerry parse(String content) {
			if (content == null) {
				content = StringPool.EMPTY;
			}
			Document doc = domBuilder.parse(content);
			return new Jerry(domBuilder, doc);
		}
	}

	/**
	 * Just creates new {@link jodd.jerry.Jerry.JerryParser Jerry runner} to separate
	 * parser creation and creation of new Jerry instances.
	 */
	public static JerryParser jerry() {
		return new JerryParser();
	}

	/**
	 * Creates new {@link jodd.jerry.Jerry.JerryParser Jerry runner} with
	 * provided implementation of {@link jodd.lagarto.dom.DOMBuilder}.
	 */
	public static JerryParser jerry(final DOMBuilder domBuilder) {
		return new JerryParser(domBuilder);
	}

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

	protected final Jerry parent;
	protected final Node[] nodes;
	protected final DOMBuilder builder;

	/**
	 * Creates root Jerry.
	 */
	protected Jerry(final DOMBuilder builder, final Node... nodes) {
		this.parent = null;
		this.nodes = nodes;
		this.builder = builder;
	}

	/**
	 * Creates child Jerry.
	 */
	protected Jerry(final Jerry parent, final Node... nodes) {
		this.parent = parent;
		this.nodes = nodes;
		this.builder = parent.builder;
	}

	/**
	 * Creates child Jerry.
	 */
	protected Jerry(final Jerry parent, final Node[] nodes1, final Node[] nodes2) {
		this.parent = parent;
		this.nodes = ArraysUtil.join(nodes1, nodes2);
		this.builder = parent.builder;
	}

	/**
	 * Creates child Jerry.
	 */
	protected Jerry(final Jerry parent, final List<Node> nodeList) {
		this(parent, nodeList.toArray(new Node[0]));
	}

	// ---------------------------------------------------------------- this

	/**
	 * Returns number of nodes in this Jerry.
	 */
	public int length() {
		return nodes.length;
	}

	/**
	 * Returns number of nodes in this Jerry.
	 */
	public int size() {
		return nodes.length;
	}

	/**
	 * Returns node at given index. Returns null
	 * if index is out of bounds.
	 */
	public Node get(final int index) {
		if ((index < 0) || (index >= nodes.length)) {
			return null;
		}
		return nodes[index];
	}

	/**
	 * Retrieve all DOM elements matched by this set.
	 * Warning: returned array is not a clone!
	 */
	public Node[] get() {
		return nodes;
	}

	/**
	 * Searches for a given Node from among the matched elements.
	 */
	public int index(final Node element) {
		if (nodes.length == 0) {
			return -1;
		}

		int index = 0;
		for (Node node : nodes) {
			if (node == element) {
				return index;
			}
			index++;
		}
		return -1;
	}

	// ---------------------------------------------------------------- Traversing

	/**
	 * Gets the immediate children of each element in the set of matched elements.
	 */
	public Jerry children() {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				Node[] children = node.getChildElements();

				Collections.addAll(result, children);
			}
		}
		return new Jerry(this, result);
	}

	/**
	* Get the children of each element in the set of matched elements,
	* including text and comment nodes.
	*/
	public Jerry contents() {
		List<Node> result = new NodeList(nodes.length);
		if (nodes.length > 0) {
			for (Node node : nodes) {
				Node[] contents = node.getChildNodes();
				Collections.addAll(result, contents);
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Gets the parent of each element in the current set of matched elements.
	 */
	public Jerry parent() {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				result.add(node.getParentNode());
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Gets the siblings of each element in the set of matched elements.
	 */
	public Jerry siblings() {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				Node[] allElements = node.getParentNode().getChildElements();
				for (Node sibling : allElements) {
					if (sibling != node) {
						result.add(sibling);
					}
				}
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Gets the immediately following sibling of each element in the
	 * set of matched elements.
	 */
	public Jerry next() {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				result.add(node.getNextSiblingElement());
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Get all following siblings of each element in the set of matched
	 * elements, optionally filtered by a selector.
	 */
	public Jerry nextAll() {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				Node currentSiblingElement = node.getNextSiblingElement();
				while (currentSiblingElement != null) {
					result.add(currentSiblingElement);
					currentSiblingElement = currentSiblingElement.getNextSiblingElement();
				}
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Gets the immediately preceding sibling of each element in the
	 * set of matched elements.
	 */
	public Jerry prev() {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				result.add(node.getPreviousSiblingElement());
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Get all preceding siblings of each element in the set of matched
	 * elements, optionally filtered by a selector.
	 */
	public Jerry prevAll() {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				Node currentSiblingElement = node.getPreviousSiblingElement();
				while (currentSiblingElement != null) {
					result.add(currentSiblingElement);
					currentSiblingElement = currentSiblingElement.getPreviousSiblingElement();
				}
			}
		}
		return new Jerry(this, result);
	}

	/**
	 *  Gets the descendants of each element in the current set of matched elements,
	 *  filtered by a selector.
	 */
	public Jerry find(final String cssSelector) {
		final List<Node> result = new NodeList();

		if (nodes.length > 0) {
			for (Node node : nodes) {
				NodeSelector nodeSelector = createNodeSelector(node);
				List<Node> filteredNodes = nodeSelector.select(cssSelector);
				result.addAll(filteredNodes);
			}
		}

		return new Jerry(this, result);
	}

	/**
	 * @see #find(String)
	 */
	public Jerry $(final String cssSelector) {
		return find(cssSelector);
	}

	/**
	 * Shortcut for context.find(css).
	 */
	public static Jerry $(final String cssSelector, final Jerry context) {
		return context.find(cssSelector);
	}

	/**
	 * Creates node selector.
	 */
	protected NodeSelector createNodeSelector(final Node node) {
		return new NodeSelector(node);
	}

	/**
	 * Iterates over a jQuery object, executing a function for
	 * each matched element.
	 * @see #eachNode(JerryNodeFunction)
	 */
	public Jerry each(final JerryFunction function) {
		for (int i = 0; i < nodes.length; i++) {
			Node node = nodes[i];
			Jerry $this = new Jerry(this, node);
			Boolean result = function.onNode($this, i);
			if (result != null && result == Boolean.FALSE) {
				break;
			}
		}
		return this;
	}

	/**
	 * Iterates over a jQuery object, executing a function for
	 * each matched element.
	 * @see #each(JerryFunction)
	 */
	public Jerry eachNode(final JerryNodeFunction function) {
		for (int i = 0; i < nodes.length; i++) {
			Node node = nodes[i];
			if (!function.onNode(node, i)) {
				break;
			}
		}
		return this;
	}

	// ---------------------------------------------------------------- Miscellaneous Traversing

	/**
	 * Adds elements to the set of matched elements.
	 */
	public Jerry add(final String selector) {
		return new Jerry(this, nodes, root().find(selector).nodes);
	}

	/**
	 * Ends the most recent filtering operation in the current chain
	 * and returns the set of matched elements to its previous state.
	 */
	public Jerry end() {
		return parent;
	}

	/**
	 * Removes elements from the set of matched elements.
	 */
	public Jerry not(final String cssSelector) {
		Node[]  notNodes = root().find(cssSelector).nodes;
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				if (!ArraysUtil.contains(notNodes, node)) {
					result.add(node);
				}
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Returns root Jerry.
	 */
	public Jerry root() {
		Jerry jerry = this.parent;
		if (jerry == null) {
			return this;
		}
		while (jerry.parent != null) {
			jerry = jerry.parent;
		}
		return jerry;
	}

	// ---------------------------------------------------------------- Filtering

	/**
	 * Reduces the set of matched elements to the first in the set.
	 */
	public Jerry first() {
		List<Node> result = new NodeList(nodes.length);
		if (nodes.length > 0) {
			result.add(nodes[0]);
		}
		return new Jerry(this, result);
	}

	/**
	 * Reduces the set of matched elements to the last in the set.
	 */
	public Jerry last() {
		List<Node> result = new NodeList(nodes.length);
		if (nodes.length > 0) {
			result.add(nodes[nodes.length - 1]);
		}
		return new Jerry(this, result);
	}

	/**
	 * Reduces the set of matched elements to the one at the specified index.
	 */
	public Jerry eq(final int value) {
		List<Node> result = new NodeList(1);
		int matchingIndex = value >= 0 ? value : nodes.length + value;

		if (nodes.length > 0) {
			int index = 0;
			for (Node node : nodes) {
				if (index == matchingIndex) {
					result.add(node);
					break;
				}
				index++;
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Reduces the set of matched elements to the one at an index greater
	 * than specified index.
	 */
	public Jerry gt(final int value) {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			int index = 0;

			for (Node node : nodes) {
				if (index > value) {
					result.add(node);
				}
				index++;
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Reduces the set of matched elements to the one at an index less
	 * than specified index.
	 */
	public Jerry lt(final int value) {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			int index = 0;
			for (Node node : nodes) {
				if (index < value) {
					result.add(node);
				}
				index++;
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Checks the current matched set of elements against a selector and
	 * return true if at least one of these elements matches
	 * the given arguments.
	 */
	public boolean is(final String cssSelectors) {
		if (nodes.length == 0) {
			return false;
		}

		for (Node node : nodes) {
			Node parentNode = node.getParentNode();
			if (parentNode == null) {
				continue;
			}

			NodeSelector nodeSelector = createNodeSelector(parentNode);
			List<Node> selectedNodes = nodeSelector.select(cssSelectors);

			for (Node selected : selectedNodes) {
				if (node == selected) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Reduces the set of matched elements to those that match the selector.
	 */
	public Jerry filter(final String cssSelectors) {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				Node parentNode = node.getParentNode();
				if (parentNode == null) {
					continue;
				}

				NodeSelector nodeSelector = createNodeSelector(parentNode);
				List<Node> selectedNodes = nodeSelector.select(cssSelectors);

				for (Node selected : selectedNodes) {
					if (node == selected) {
						result.add(node);
					}
				}
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Reduces the set of matched elements to those that pass the
	 * {@link JerryFunction function's} test.
	 */
	public Jerry filter(final JerryFunction jerryFunction) {
		List<Node> result = new NodeList(nodes.length);

		for (int i = 0; i < nodes.length; i++) {
			Node node = nodes[i];
			Node parentNode = node.getParentNode();
			if (parentNode == null) {
				continue;
			}

			Jerry $this = new Jerry(this, node);

			boolean accept = jerryFunction.onNode($this, i);

			if (accept) {
				result.add(node);
			}
		}
		return new Jerry(this, result);
	}

	/**
	 * Reduce the set of matched elements to those that have a descendant that
	 * matches the selector or DOM element.
	 */
	public Jerry has(final String cssSelectors) {
		List<Node> result = new NodeList(nodes.length);

		if (nodes.length > 0) {
			for (Node node : nodes) {
				NodeSelector nodeSelector = createNodeSelector(node);
				List<Node> selectedNodes = nodeSelector.select(cssSelectors);

				if (!selectedNodes.isEmpty()) {
					result.add(node);
				}
			}
		}

		return new Jerry(this, result);
	}

	// ---------------------------------------------------------------- Attributes

	/**
	 * Gets the value of an attribute for the first element in the set of matched elements.
	 * Returns null if set is empty.
	 */
	public String attr(final String name) {
		if (nodes.length == 0) {
			return null;
		}
		if (name == null) {
			return null;
		}
		return nodes[0].getAttribute(name);
	}

	/**
	 * Sets one or more attributes for the set of matched elements.
	 */
	public Jerry attr(final String name, final String value) {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			node.setAttribute(name, value);
		}
		return this;
	}

	/**
	 * Removes an attribute from each element in the set of matched elements.
	 */
	public Jerry removeAttr(final String name) {
		if (name == null) {
			return this;
		}
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			node.removeAttribute(name);
		}
		return this;
	}

	/**
	 * Gets the value of a style property for the first element
	 * in the set of matched elements. Returns null
	 * if set is empty.
	 */
	public String css(String propertyName) {
		if (nodes.length == 0) {
			return null;
		}

		propertyName = Format.fromCamelCase(propertyName, '-');

		String styleAttrValue = nodes[0].getAttribute("style");
		if (styleAttrValue == null) {
			return null;
		}

		Map<String, String> styles = createPropertiesMap(styleAttrValue, ';', ':');
		return styles.get(propertyName);
	}

	/**
	 * Sets one or more CSS properties for the set of matched elements.
	 * By passing an empty value, that property will be removed.
	 * Note that this is different from jQuery, where this means
	 * that property will be reset to previous value if existed.
	 */
	public Jerry css(String propertyName, final String value) {
		propertyName = Format.fromCamelCase(propertyName, '-');

		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			String styleAttrValue = node.getAttribute("style");
			Map<String, String> styles = createPropertiesMap(styleAttrValue, ';', ':');
			if (value.length() == 0) {
				styles.remove(propertyName);
			} else {
				styles.put(propertyName, value);
			}

			styleAttrValue = generateAttributeValue(styles, ';', ':');
			node.setAttribute("style", styleAttrValue);
		}
		return this;
	}

	/**
	 * Sets one or more CSS properties for the set of matched elements.
	 */
	public Jerry css(final String... css) {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			String styleAttrValue = node.getAttribute("style");
			Map<String, String> styles = createPropertiesMap(styleAttrValue, ';', ':');

			for (int i = 0; i < css.length; i += 2) {
				String propertyName = css[i];
				propertyName = Format.fromCamelCase(propertyName, '-');
				String value = css[i + 1];
				if (value.length() == 0) {
					styles.remove(propertyName);
				} else {
					styles.put(propertyName, value);
				}
			}
			styleAttrValue = generateAttributeValue(styles, ';', ':');
			node.setAttribute("style", styleAttrValue);
		}
		return this;
	}

	/**
	 * Adds the specified class(es) to each of the set of matched elements.
	 */
	public Jerry addClass(final String... classNames) {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			String attrClass = node.getAttribute("class");
			Set<String> classes = createPropertiesSet(attrClass, ' ');
			boolean wasChange = false;
			for (String className : classNames) {
				if (classes.add(className)) {
					wasChange = true;
				}
			}
			if (wasChange) {
				String attrValue = generateAttributeValue(classes, ' ');
				node.setAttribute("class", attrValue);
			}
		}
		return this;
	}

	/**
	 * Determines whether any of the matched elements are assigned the given class.
	 */
	public boolean hasClass(final String... classNames) {
		if (nodes.length == 0) {
			return false;
		}
		for (Node node : nodes) {
			String attrClass = node.getAttribute("class");
			Set<String> classes = createPropertiesSet(attrClass, ' ');
			for (String className : classNames) {
				if (classes.contains(className)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Removes a single class, multiple classes, or all classes
	 * from each element in the set of matched elements.
	 */
	public Jerry removeClass(final String... classNames) {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			String attrClass = node.getAttribute("class");
			Set<String> classes = createPropertiesSet(attrClass, ' ');
			boolean wasChange = false;
			for (String className : classNames) {
				if (classes.remove(className)) {
					wasChange = true;
				}
			}
			if (wasChange) {
				String attrValue = generateAttributeValue(classes, ' ');
				node.setAttribute("class", attrValue);
			}
		}
		return this;
	}

	/**
	 * Adds or remove one or more classes from each element in the set of
	 * matched elements, depending on either the class's presence or
	 * the value of the switch argument.
	 */
	public Jerry toggleClass(final String... classNames) {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			String attrClass = node.getAttribute("class");
			Set<String> classes = createPropertiesSet(attrClass, ' ');
			for (String className : classNames) {
				if (classes.contains(className)) {
					classes.remove(className);
				} else {
					classes.add(className);
				}
			}
			String attrValue = generateAttributeValue(classes, ' ');
			node.setAttribute("class", attrValue);
		}
		return this;
	}

	// ---------------------------------------------------------------- content

	/**
	 * Gets the combined text contents of each element in the set of
	 * matched elements, including their descendants.
	 * Text is HTML decoded for text nodes.
	 */
	public String text() {
		if (nodes.length == 0) {
			return StringPool.EMPTY;
		}

		StringBuilder sb = new StringBuilder();
		for (Node node : nodes) {
			sb.append(node.getTextContent());
		}
		return sb.toString();
	}

	/**
	 * Sets the content of each element in the set of matched elements to the specified text.
	 */
	public Jerry text(String text) {
		if (nodes.length == 0) {
			return this;
		}
		if (text == null) {
			text = StringPool.EMPTY;
		}
		for (Node node : nodes) {
			node.removeAllChilds();
			Text textNode = new Text(node.getOwnerDocument(), text);
			node.addChild(textNode);
		}
		return this;
	}

	/**
	 * Gets the HTML contents of the first element in the set of matched elements.
	 * Content is raw, not HTML decoded.
	 * @see #htmlAll(boolean)
	 */
	public String html() {
		if (nodes.length == 0) {
			return null;
		}
		return nodes[0].getInnerHtml();
	}

	/**
	 * Gets the combined HTML contents of each element in the set of
	 * matched elements, including their descendants.
	 * @see #html()
	 * @param setIncluded if true than sets node are included in the output
	 */
	public String htmlAll(final boolean setIncluded) {
		if (nodes.length == 0) {
			return StringPool.EMPTY;
		}
		StringBuilder sb = new StringBuilder();
		for (Node node : nodes) {
			sb.append(setIncluded ? node.getHtml() : node.getInnerHtml());
		}
		return sb.toString();
	}

	/**
	 * Sets the HTML contents of each element in the set of matched elements.
	 */
	public Jerry html(String html) {
		if (html == null) {
			html = StringPool.EMPTY;
		}

		final Document doc = builder.parse(html);

		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			node.removeAllChilds();

			// clone to preserve for next iteration
			// as nodes will be detached from parent
			Document workingDoc = doc.clone();

			node.addChild(workingDoc.getChildNodes());
		}
		return this;
	}

	// ---------------------------------------------------------------- DOM

	/**
	 * Inserts content, specified by the parameter, to the end of each
	 * element in the set of matched elements.
	 */
	public Jerry append(String html) {
		if (html == null) {
			html = StringPool.EMPTY;
		}
		final Document doc = builder.parse(html);

		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			Document workingDoc = doc.clone();
			node.addChild(workingDoc.getChildNodes());
		}
		return this;
	}

	/**
	 * Insert content, specified by the parameter, to the beginning of each
	 * element in the set of matched elements.
	 */
	public Jerry prepend(String html) {
		if (html == null) {
			html = StringPool.EMPTY;
		}
		final Document doc = builder.parse(html);

		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			Document workingDoc = doc.clone();
			node.insertChild(workingDoc.getChildNodes(), 0);
		}
		return this;
	}

	/**
	 * Inserts content, specified by the parameter, before each
	 * element in the set of matched elements.
	 */
	public Jerry before(String html) {
		if (html == null) {
			html = StringPool.EMPTY;
		}
		final Document doc = builder.parse(html);
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			Document workingDoc = doc.clone();
			node.insertBefore(workingDoc.getChildNodes(), node);
		}
		return this;
	}

	/**
	 * Inserts content, specified by the parameter, after each
	 * element in the set of matched elements.
	 */
	public Jerry after(String html) {
		if (html == null) {
			html = StringPool.EMPTY;
		}
		final Document doc = builder.parse(html);
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			Document workingDoc = doc.clone();
			node.insertAfter(workingDoc.getChildNodes(), node);
		}
		return this;
	}

	/**
	 * Replace each element in the set of matched elements with the provided
	 * new content and return the set of elements that was removed.
	 */
	public Jerry replaceWith(String html) {
 		if (html == null) {
			html = StringPool.EMPTY;
		}
		final Document doc = builder.parse(html);

		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			Node parent = node.getParentNode();
			// if a node already is the root element, don't unwrap
			if (parent == null) {
				continue;
			}

			// replace, if possible
			Document workingDoc = doc.clone();
			int index = node.getSiblingIndex();
			parent.insertChild(workingDoc.getFirstChild(), index);
			node.detachFromParent();
		}

		return this;
	}

	/**
	 * Removes the set of matched elements from the DOM.
	 */
	public Jerry remove() {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			node.detachFromParent();
		}
		return this;
	}

	/**
	 * Removes the set of matched elements from the DOM.
	 * Identical to {@link #remove()}.
	 */
	public Jerry detach() {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			node.detachFromParent();
		}
		return this;
	}

	/**
	 * Removes all child nodes of the set of matched elements from the DOM.
	 */
	public Jerry empty() {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			node.removeAllChilds();
		}
		return this;
	}

	// ---------------------------------------------------------------- wrap

	/**
	 * Wraps an HTML structure around each element in the set of matched elements.
	 * Returns the original set of elements for chaining purposes.
	 */
	public Jerry wrap(String html) {
		if (html == null) {
			html = StringPool.EMPTY;
		}
		final Document doc = builder.parse(html);

		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			Document workingDoc = doc.clone();
			Node inmostNode = workingDoc;
			while (inmostNode.hasChildNodes()) {
				inmostNode = inmostNode.getFirstChild();
			}

			// replace
			Node parent = node.getParentNode();
			int index = node.getSiblingIndex();
			inmostNode.addChild(node);
			parent.insertChild(workingDoc.getFirstChild(), index);
		}

		return this;
	}

	/**
	 * Remove the parents of the set of matched elements from the DOM, leaving
	 * the matched elements (and siblings, if any) in their place.
	 */
	public Jerry unwrap() {
		if (nodes.length == 0) {
			return this;
		}
		for (Node node : nodes) {
			Node parent = node.getParentNode();
			// if a node already is the root element, don't unwrap
			if (parent == null) {
				continue;
			}

			// replace, if possible
			Node grandparent = parent.getParentNode();
			if (grandparent == null) {
				continue;
			}

			Node[] siblings = parent.getChildNodes();
			int index = parent.getSiblingIndex();
			grandparent.insertChild(siblings, index);
			parent.detachFromParent();
		}

		return this;
	}

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

	/**
	 * Returns iterator over nodes contained in the Jerry object.
	 * Each node is wrapped. Similar to {@link #each(JerryFunction)}.
	 */
	@Override
	public Iterator<Jerry> iterator() {
		final Jerry jerry = this;

		return new Iterator<Jerry>() {
			private int index = 0;

			@Override
			public boolean hasNext() {
				return index < jerry.nodes.length;
			}

			@Override
			public Jerry next() {
				if (!hasNext()) {
					throw new NoSuchElementException();
				}
				Jerry nextJerry = new Jerry(jerry, jerry.get(index));
				index++;
				return nextJerry;
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	// ---------------------------------------------------------------- form

	/**
	 * Processes all forms, collects all form parameters and calls back the
	 * {@link JerryFormHandler}.
	 */
	public Jerry form(final String formCssSelector, final JerryFormHandler jerryFormHandler) {
		Jerry form = find(formCssSelector);

		// process each form
		for (Node node : form.nodes) {
			Jerry singleForm = new Jerry(this, node);

			final Map<String, String[]> parameters = new HashMap<>();

			// process all input elements

			singleForm.$("input").each(($inputTag, index) -> {

				String type = $inputTag.attr("type");

				// An input element with no type attribute specified represents
				// the same thing as an input element with its type attribute set to "text".

				if (type == null) {
					type = "text";
				}

				boolean isCheckbox = type.equals("checkbox");
				boolean isRadio = type.equals("radio");

				if (isRadio || isCheckbox) {
					if (!($inputTag.nodes[0].hasAttribute("checked"))) {
						return true;
					}
				}

				String name = $inputTag.attr("name");
				if (name == null) {
					return true;
				}

				String tagValue = $inputTag.attr("value");

				if (tagValue == null) {
					if (isCheckbox) {
						tagValue = "on";
					}
				}

				// add tag value
				String[] value = parameters.get(name);

				if (value == null) {
					value = new String[] {tagValue};
				} else {
					value = ArraysUtil.append(value, tagValue);
				}

				parameters.put(name, value);
				return true;
			});

			// process all select elements

			singleForm.$("select").each(($selectTag, index) -> {
				final String name = $selectTag.attr("name");

				$selectTag.$("option").each(($optionTag, index1) -> {
					if ($optionTag.nodes[0].hasAttribute("selected")) {
						String tagValue = $optionTag.attr("value");

						// add tag value
						String[] value = parameters.get(name);

						if (value == null) {
							value = new String[] {tagValue};
						} else {
							value = ArraysUtil.append(value, tagValue);
						}

						parameters.put(name, value);
					}
					return true;
				});

				return true;
			});

			// process all text areas

			singleForm.$("textarea").each(($textarea, index) -> {
				String name = $textarea.attr("name");
				String value = $textarea.text();

				parameters.put(name, new String[] {value});
				return true;
			});

			// done

			jerryFormHandler.onForm(singleForm, parameters);
		}

		return this;
	}

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

	protected Set<String> createPropertiesSet(final String attrValue, final char propertiesDelimiter) {
		if (attrValue == null) {
			return new LinkedHashSet<>();
		}
		String[] properties = StringUtil.splitc(attrValue, propertiesDelimiter);
		LinkedHashSet<String> set = new LinkedHashSet<>(properties.length);

		Collections.addAll(set, properties);
		return set;
	}

	protected String generateAttributeValue(final Set<String> set, final char propertiesDelimiter) {
		StringBuilder sb = new StringBuilder(set.size() * 16);
		boolean first = true;
		for (String entry : set) {
			if (!first) {
				sb.append(propertiesDelimiter);
			} else {
				first = false;
			}
			sb.append(entry);
		}
		return sb.toString();
	}

	protected Map<String, String> createPropertiesMap(final String attrValue, final char propertiesDelimiter, final char valueDelimiter) {
		if (attrValue == null) {
			return new LinkedHashMap<>();
		}
		String[] properties = StringUtil.splitc(attrValue, propertiesDelimiter);
		LinkedHashMap<String, String> map = new LinkedHashMap<>(properties.length);
		for (String property : properties) {
			int valueDelimiterIndex = property.indexOf(valueDelimiter);
			if (valueDelimiterIndex != -1) {
				String propertyName = property.substring(0, valueDelimiterIndex).trim();
				String propertyValue = property.substring(valueDelimiterIndex + 1).trim();
				map.put(propertyName, propertyValue);
			}
		}
		return map;
	}

	protected String generateAttributeValue(final Map<String, String> map, final char propertiesDelimiter, final char valueDelimiter) {
		StringBuilder sb = new StringBuilder(map.size() * 32);
		for (Map.Entry<String, String> entry : map.entrySet()) {
			sb.append(entry.getKey());
			sb.append(valueDelimiter);
			sb.append(entry.getValue());
			sb.append(propertiesDelimiter);
		}
		return sb.toString();
	}

}


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

package jodd.buffer;

import java.util.Arrays;

/**
 * Faster {@code float} buffer.
 */
public class FastFloatBuffer {

	private float[] buffer;
	private int offset;

	/**
	 * Creates a new {@code float} buffer. The buffer capacity is
	 * initially 64 floats, though its size increases if necessary.
	 */
	public FastFloatBuffer() {
		this.buffer = new float[64];
	}

	/**
	 * Creates a new {@code float} buffer, with a buffer capacity of
	 * the specified size.
	 *
	 * @param size the initial size.
	 * @throws IllegalArgumentException if size is negative.
	 */
	public FastFloatBuffer(final int size) {
		this.buffer = new float[size];
	}

	/**
	 * Grows the buffer.
	 */
	private void grow(final int minCapacity) {
		final int oldCapacity = buffer.length;
		int newCapacity = oldCapacity << 1;
		if (newCapacity - minCapacity < 0) {
			// special case, min capacity is larger then a grow
			newCapacity = minCapacity + 512;
		}
		buffer = Arrays.copyOf(buffer, newCapacity);
	}

	/**
	 * Appends single {@code float} to buffer.
	 */
	public void append(final float element) {
		if (offset - buffer.length >= 0) {
			grow(offset);
		}

		buffer[offset++] = element;
	}

	/**
	 * Appends {@code float} array to buffer.
	 */
	public FastFloatBuffer append(final float[] array, final int off, final int len) {
		if (offset + len - buffer.length > 0) {
			grow(offset + len);
		}

		System.arraycopy(array, off, buffer, offset, len);
		offset += len;
		return this;
	}

	/**
	 * Appends {@code float} array to buffer.
	 */
	public FastFloatBuffer append(final float[] array) {
		return append(array, 0, array.length);
	}

	/**
	 * Appends another fast buffer to this one.
	 */
	public FastFloatBuffer append(final FastFloatBuffer buff) {
		if (buff.offset == 0) {
			return this;
		}
		append(buff.buffer, 0, buff.offset);
		return this;
	}

	/**
	 * Returns buffer size.
	 */
	public int size() {
		return offset;
	}

	/**
	 * Tests if this buffer has no elements.
	 */
	public boolean isEmpty() {
		return offset == 0;
	}

	/**
	 * Resets the buffer content.
	 */
	public void clear() {
		offset = 0;
	}

	/**
	 * Creates {@code float} array from buffered content.
	 */
	public float[] toArray() {
		return Arrays.copyOf(buffer, offset);
	}

	/**
	 * Creates {@code float} subarray from buffered content.
	 */
	public float[] toArray(final int start, final int len) {
		final float[] array = new float[len];

		if (len == 0) {
			return array;
		}

		System.arraycopy(buffer, start, array, 0, len);

		return array;
	}

	/**
	 * Returns {@code float} element at given index.
	 */
	public float get(final int index) {
		if (index >= offset) {
			throw new IndexOutOfBoundsException();
		}
		return buffer[index];
	}

}