/*
 * Copyright 2003 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.beans;

import java.beans.PropertyDescriptor;
import java.security.ProtectionDomain;
import java.util.*;
import net.sf.cglib.core.*;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Type;

/**
 * @author Juozas Baliuka, Chris Nokleberg
 */
public class BeanGenerator extends AbstractClassGenerator
{
    private static final Source SOURCE = new Source(BeanGenerator.class.getName());
    private static final BeanGeneratorKey KEY_FACTORY =
      (BeanGeneratorKey)KeyFactory.create(BeanGeneratorKey.class);

    interface BeanGeneratorKey {
        public Object newInstance(String superclass, Map props);
    }

    private Class superclass;
    private Map props = new HashMap();
    private boolean classOnly;

    public BeanGenerator() {
        super(SOURCE);
    }

    /**
     * Set the class which the generated class will extend. The class
     * must not be declared as final, and must have a non-private
     * no-argument constructor.
     * @param superclass class to extend, or null to extend Object
     */
    public void setSuperclass(Class superclass) {
        if (superclass != null && superclass.equals(Object.class)) {
            superclass = null;
        }
        this.superclass = superclass;
    }

    public void addProperty(String name, Class type) {
        if (props.containsKey(name)) {
            throw new IllegalArgumentException("Duplicate property name \"" + name + "\"");
        }
        props.put(name, Type.getType(type));
    }

    protected ClassLoader getDefaultClassLoader() {
        if (superclass != null) {
            return superclass.getClassLoader();
        } else {
            return null;
        }
    }

    protected ProtectionDomain getProtectionDomain() {
        return ReflectUtils.getProtectionDomain(superclass);
    }

    public Object create() {
        classOnly = false;
        return createHelper();
    }

    public Object createClass() {
        classOnly = true;
        return createHelper();
    }

    private Object createHelper() {
        if (superclass != null) {
            setNamePrefix(superclass.getName());
        }
        String superName = (superclass != null) ? superclass.getName() : "java.lang.Object";
        Object key = KEY_FACTORY.newInstance(superName, props);
        return super.create(key);
    }

    public void generateClass(ClassVisitor v) throws Exception {
        int size = props.size();
        String[] names = (String[])props.keySet().toArray(new String[size]);
        Type[] types = new Type[size];
        for (int i = 0; i < size; i++) {
            types[i] = (Type)props.get(names[i]);
        }
        ClassEmitter ce = new ClassEmitter(v);
        ce.begin_class(Constants.V1_2,
                       Constants.ACC_PUBLIC,
                       getClassName(),
                       superclass != null ? Type.getType(superclass) : Constants.TYPE_OBJECT,
                       null,
                       null);
        EmitUtils.null_constructor(ce);
        EmitUtils.add_properties(ce, names, types);
        ce.end_class();
    }

    protected Object firstInstance(Class type) {
        if (classOnly) {
            return type;
        } else {
            return ReflectUtils.newInstance(type);
        }
    }

    protected Object nextInstance(Object instance) {
        Class protoclass = (instance instanceof Class) ? (Class)instance : instance.getClass();
        if (classOnly) {
            return protoclass;
        } else {
            return ReflectUtils.newInstance(protoclass);
        }
    }

    public static void addProperties(BeanGenerator gen, Map props) {
        for (Iterator it = props.keySet().iterator(); it.hasNext();) {
            String name = (String)it.next();
            gen.addProperty(name, (Class)props.get(name));
        }
    }

    public static void addProperties(BeanGenerator gen, Class type) {
        addProperties(gen, ReflectUtils.getBeanProperties(type));
    }

    public static void addProperties(BeanGenerator gen, PropertyDescriptor[] descriptors) {
        for (int i = 0; i < descriptors.length; i++) {
            gen.addProperty(descriptors[i].getName(), descriptors[i].getPropertyType());
        }
    }
}


/*
 * Copyright 2003 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.transform.impl;

import net.sf.cglib.transform.*;
import java.util.*;
import net.sf.cglib.core.*;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.Label;
import org.objectweb.asm.Type;

public class FieldProviderTransformer extends ClassEmitterTransformer {

    private static final String FIELD_NAMES = "CGLIB$FIELD_NAMES";
    private static final String FIELD_TYPES = "CGLIB$FIELD_TYPES";

    private static final Type FIELD_PROVIDER =
      TypeUtils.parseType("net.sf.cglib.transform.impl.FieldProvider");
    private static final Type ILLEGAL_ARGUMENT_EXCEPTION =
      TypeUtils.parseType("IllegalArgumentException");
    private static final Signature PROVIDER_GET =
      TypeUtils.parseSignature("Object getField(String)");
    private static final Signature PROVIDER_SET =
      TypeUtils.parseSignature("void setField(String, Object)");
    private static final Signature PROVIDER_SET_BY_INDEX =
      TypeUtils.parseSignature("void setField(int, Object)");
    private static final Signature PROVIDER_GET_BY_INDEX =
      TypeUtils.parseSignature("Object getField(int)");
    private static final Signature PROVIDER_GET_TYPES =
      TypeUtils.parseSignature("Class[] getFieldTypes()");
    private static final Signature PROVIDER_GET_NAMES =
      TypeUtils.parseSignature("String[] getFieldNames()");

    private int access;
    private Map fields;

    public void begin_class(int version, int access, String className, Type superType, Type[] interfaces, String sourceFile) {
        if (!TypeUtils.isAbstract(access)) {
            interfaces = TypeUtils.add(interfaces, FIELD_PROVIDER);
        }
        this.access = access;
        fields = new HashMap();
        super.begin_class(version, access, className, superType, interfaces, sourceFile);
    }

    public void declare_field(int access, String name, Type type, Object value) {
        super.declare_field(access, name, type, value);

        if (!TypeUtils.isStatic(access)) {
            fields.put(name, type);
        }
    }

    public void end_class() {
        if (!TypeUtils.isInterface(access)) {
            try {
                generate();
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new CodeGenerationException(e);
            }
        }
        super.end_class();
    }

    private void generate() throws Exception {
        final String[] names = (String[])fields.keySet().toArray(new String[fields.size()]);

        int indexes[] = new int[names.length];
        for (int i = 0; i < indexes.length; i++) {
            indexes[i] = i;
        }

        super.declare_field(Constants.PRIVATE_FINAL_STATIC, FIELD_NAMES, Constants.TYPE_STRING_ARRAY, null);
        super.declare_field(Constants.PRIVATE_FINAL_STATIC, FIELD_TYPES, Constants.TYPE_CLASS_ARRAY, null);

        // use separate methods here because each process switch inner class needs a final CodeEmitter
        initFieldProvider(names);
        getNames();
        getTypes();
        getField(names);
        setField(names);
        setByIndex(names, indexes);
        getByIndex(names, indexes);
    }

    private void initFieldProvider(String[] names) {
        CodeEmitter e = getStaticHook();
        EmitUtils.push_object(e, names);
        e.putstatic(getClassType(), FIELD_NAMES, Constants.TYPE_STRING_ARRAY);

        e.push(names.length);
        e.newarray(Constants.TYPE_CLASS);
        e.dup();
        for(int i = 0; i < names.length; i++ ){
            e.dup();
            e.push(i);
            Type type = (Type)fields.get(names[i]);
            EmitUtils.load_class(e, type);
            e.aastore();
        }
        e.putstatic(getClassType(), FIELD_TYPES, Constants.TYPE_CLASS_ARRAY);
    }

    private void getNames() {
        CodeEmitter e = super.begin_method(Constants.ACC_PUBLIC, PROVIDER_GET_NAMES, null);
        e.getstatic(getClassType(), FIELD_NAMES, Constants.TYPE_STRING_ARRAY);
        e.return_value();
        e.end_method();
    }

    private void getTypes() {
        CodeEmitter e = super.begin_method(Constants.ACC_PUBLIC, PROVIDER_GET_TYPES, null);
        e.getstatic(getClassType(), FIELD_TYPES, Constants.TYPE_CLASS_ARRAY);
        e.return_value();
        e.end_method();
    }

    private void setByIndex(final String[] names, final int[] indexes) throws Exception {
        final CodeEmitter e = super.begin_method(Constants.ACC_PUBLIC, PROVIDER_SET_BY_INDEX, null);
        e.load_this();
        e.load_arg(1);
        e.load_arg(0);
        e.process_switch(indexes, new ProcessSwitchCallback() {
            public void processCase(int key, Label end) throws Exception {
                Type type = (Type)fields.get(names[key]);
                e.unbox(type);
                e.putfield(names[key]);
                e.return_value();
            }
            public void processDefault() throws Exception {
                e.throw_exception(ILLEGAL_ARGUMENT_EXCEPTION, "Unknown field index");
            }
        });
        e.end_method();
    }

    private void getByIndex(final String[] names, final int[] indexes) throws Exception {
        final CodeEmitter e = super.begin_method(Constants.ACC_PUBLIC, PROVIDER_GET_BY_INDEX, null);
        e.load_this();
        e.load_arg(0);
        e.process_switch(indexes, new ProcessSwitchCallback() {
            public void processCase(int key, Label end) throws Exception {
                Type type = (Type)fields.get(names[key]);
                e.getfield(names[key]);
                e.box(type);
                e.return_value();
            }
            public void processDefault() throws Exception {
                e.throw_exception(ILLEGAL_ARGUMENT_EXCEPTION, "Unknown field index");
            }
        });
        e.end_method();
    }

    // TODO: if this is used to enhance class files SWITCH_STYLE_TRIE should be used
    // to avoid JVM hashcode implementation incompatibilities
    private void getField(String[] names) throws Exception {
        final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, PROVIDER_GET, null);
        e.load_this();
        e.load_arg(0);
        EmitUtils.string_switch(e, names, Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() {
            public void processCase(Object key, Label end) {
                Type type = (Type)fields.get(key);
                e.getfield((String)key);
                e.box(type);
                e.return_value();
            }
            public void processDefault() {
                e.throw_exception(ILLEGAL_ARGUMENT_EXCEPTION, "Unknown field name");
            }
        });
        e.end_method();
    }

    private void setField(String[] names) throws Exception {
        final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, PROVIDER_SET, null);
        e.load_this();
        e.load_arg(1);
        e.load_arg(0);
        EmitUtils.string_switch(e, names, Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() {
            public void processCase(Object key, Label end) {
                Type type = (Type)fields.get(key);
                e.unbox(type);
                e.putfield((String)key);
                e.return_value();
            }
            public void processDefault() {
                e.throw_exception(ILLEGAL_ARGUMENT_EXCEPTION, "Unknown field name");
            }
        });
        e.end_method();
    }
}


/*
 * Copyright 2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.proxy;

import java.lang.reflect.*;
import java.util.*;
import net.sf.cglib.core.*;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Type;

/**
 * Generates new interfaces at runtime.
 * By passing a generated interface to the Enhancer's list of interfaces to
 * implement, you can make your enhanced classes handle an arbitrary set
 * of method signatures.
 * @author Chris Nokleberg
 * @version $Id: InterfaceMaker.java,v 1.4 2006/03/05 02:43:19 herbyderby Exp $
 */
public class InterfaceMaker extends AbstractClassGenerator
{
    private static final Source SOURCE = new Source(InterfaceMaker.class.getName());
    private Map signatures = new HashMap();

    /**
     * Create a new InterfaceMaker. A new InterfaceMaker
     * object should be used for each generated interface, and should not
     * be shared across threads.
     */
    public InterfaceMaker() {
        super(SOURCE);
    }

    /**
     * Add a method signature to the interface.
     * @param sig the method signature to add to the interface
     * @param exceptions an array of exception types to declare for the method
     */
    public void add(Signature sig, Type[] exceptions) {
        signatures.put(sig, exceptions);
    }

    /**
     * Add a method signature to the interface. The method modifiers are ignored,
     * since interface methods are by definition abstract and public.
     * @param method the method to add to the interface
     */
    public void add(Method method) {
        add(ReflectUtils.getSignature(method),
            ReflectUtils.getExceptionTypes(method));
    }

    /**
     * Add all the public methods in the specified class.
     * Methods from superclasses are included, except for methods declared in the base
     * Object class (e.g. getClass, equals, hashCode).
     * @param class the class containing the methods to add to the interface
     */
    public void add(Class clazz) {
        Method[] methods = clazz.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            if (!m.getDeclaringClass().getName().equals("java.lang.Object")) {
                add(m);
            }
        }
    }

    /**
     * Create an interface using the current set of method signatures.
     */
    public Class create() {
        setUseCache(false);
        return (Class)super.create(this);
    }

    protected ClassLoader getDefaultClassLoader() {
        return null;
    }

    protected Object firstInstance(Class type) {
        return type;
    }

    protected Object nextInstance(Object instance) {
        throw new IllegalStateException("InterfaceMaker does not cache");
    }

    public void generateClass(ClassVisitor v) throws Exception {
        ClassEmitter ce = new ClassEmitter(v);
        ce.begin_class(Constants.V1_2,
                       Constants.ACC_PUBLIC | Constants.ACC_INTERFACE,
                       getClassName(),
                       null,
                       null,
                       Constants.SOURCE_FILE);
        for (Iterator it = signatures.keySet().iterator(); it.hasNext();) {
            Signature sig = (Signature)it.next();
            Type[] exceptions = (Type[])signatures.get(sig);
            ce.begin_method(Constants.ACC_PUBLIC | Constants.ACC_ABSTRACT,
                            sig,
                            exceptions).end_method();
        }
        ce.end_class();
    }
}


/*
 * Copyright 2003 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.core;

@Deprecated
public class TinyBitSet {
    private static int[] T = new int[256];
    private int value = 0;

    private static int gcount(int x) {
        int c = 0;
        while (x != 0) {
            c++;
            x &= (x - 1);
        }
        return c;
    }

    static {
        for (int j = 0; j < 256; j++) {
            T[j] = gcount(j);
        }
    }

    private static int topbit(int i) {
        int j;
        for (j = 0; i != 0; i ^= j) {
            j = i & -i;
        }
        return j;
    }

    private static int log2(int i) {
        int j = 0;
        for (j = 0; i != 0; i >>= 1) {
            j++;
        }
        return j;
    }

    public int length() {
        return log2(topbit(value));
    }

    /**
     * If bit 31 is set then this method results in an infinite loop.
     *
     * @return the number of bits set to true in this TinyBitSet.
     */
    public int cardinality() {
        int w = value;
        int c = 0;
        while (w != 0) {
            c += T[w & 255];
            w >>= 8;
        }
        return c;
    }

    public boolean get(int index) {
        return (value & (1 << index)) != 0;
    }

    public void set(int index) {
        value |= (1 << index);
    }

    public void clear(int index) {
        value &= ~(1 << index);
    }
}


/*
 * Copyright 2003,2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.core;

import net.sf.cglib.core.internal.Function;
import net.sf.cglib.core.internal.LoadingCache;
import org.objectweb.asm.ClassReader;

import java.lang.ref.WeakReference;
import java.security.ProtectionDomain;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.WeakHashMap;

/**
 * Abstract class for all code-generating CGLIB utilities.
 * In addition to caching generated classes for performance, it provides hooks for
 * customizing the ClassLoader, name of the generated class, and transformations
 * applied before generation.
 */
abstract public class AbstractClassGenerator<T>
implements ClassGenerator
{
    private static final ThreadLocal CURRENT = new ThreadLocal();

    private static volatile Map<ClassLoader, ClassLoaderData> CACHE = new WeakHashMap<ClassLoader, ClassLoaderData>();

    private static final boolean DEFAULT_USE_CACHE =
        Boolean.parseBoolean(System.getProperty("cglib.useCache", "true"));

    private GeneratorStrategy strategy = DefaultGeneratorStrategy.INSTANCE;
    private NamingPolicy namingPolicy = DefaultNamingPolicy.INSTANCE;
    private Source source;
    private ClassLoader classLoader;
    private String namePrefix;
    private Object key;
    private boolean useCache = DEFAULT_USE_CACHE;
    private String className;
    private boolean attemptLoad;

    protected static class ClassLoaderData {
        private final Set<String> reservedClassNames = new HashSet<String>();

        /**
         * {@link AbstractClassGenerator} here holds "cache key" (e.g. {@link net.sf.cglib.proxy.Enhancer}
         * configuration), and the value is the generated class plus some additional values
         * (see {@link #unwrapCachedValue(Object)}.
         *
The generated classes can be reused as long as their classloader is reachable.

         *
Note: the only way to access a class is to find it through generatedClasses cache, thus
         * the key should not expire as long as the class itself is alive (its classloader is alive).

         */
        private final LoadingCache<AbstractClassGenerator, Object, Object> generatedClasses;

        /**
         * Note: ClassLoaderData object is stored as a value of {@code WeakHashMap<ClassLoader, ...>} thus
         * this classLoader reference should be weak otherwise it would make classLoader strongly reachable
         * and alive forever.
         * Reference queue is not required since the cleanup is handled by {@link WeakHashMap}.
         */
        private final WeakReference<ClassLoader> classLoader;

        private final Predicate uniqueNamePredicate = new Predicate() {
            public boolean evaluate(Object name) {
                return reservedClassNames.contains(name);
            }
        };

        private static final Function<AbstractClassGenerator, Object> GET_KEY = new Function<AbstractClassGenerator, Object>() {
            public Object apply(AbstractClassGenerator gen) {
                return gen.key;
            }
        };

        public ClassLoaderData(ClassLoader classLoader) {
            if (classLoader == null) {
                throw new IllegalArgumentException("classLoader == null is not yet supported");
            }
            this.classLoader = new WeakReference<ClassLoader>(classLoader);
            Function<AbstractClassGenerator, Object> load =
                    new Function<AbstractClassGenerator, Object>() {
                        public Object apply(AbstractClassGenerator gen) {
                            Class klass = gen.generate(ClassLoaderData.this);
                            return gen.wrapCachedClass(klass);
                        }
                    };
            generatedClasses = new LoadingCache<AbstractClassGenerator, Object, Object>(GET_KEY, load);
        }

        public ClassLoader getClassLoader() {
            return classLoader.get();
        }

        public void reserveName(String name) {
            reservedClassNames.add(name);
        }

        public Predicate getUniqueNamePredicate() {
            return uniqueNamePredicate;
        }

        public Object get(AbstractClassGenerator gen, boolean useCache) {
            if (!useCache) {
              return gen.generate(ClassLoaderData.this);
            } else {
              Object cachedValue = generatedClasses.get(gen);
              return gen.unwrapCachedValue(cachedValue);
            }
        }
    }

    protected T wrapCachedClass(Class klass) {
        return (T) new WeakReference(klass);
    }

    protected Object unwrapCachedValue(T cached) {
        return ((WeakReference) cached).get();
    }

    protected static class Source {
        String name;
        public Source(String name) {
            this.name = name;
        }
    }

    protected AbstractClassGenerator(Source source) {
        this.source = source;
    }

    protected void setNamePrefix(String namePrefix) {
        this.namePrefix = namePrefix;
    }

    final protected String getClassName() {
        return className;
    }

    private void setClassName(String className) {
        this.className = className;
    }

    private String generateClassName(Predicate nameTestPredicate) {
        return namingPolicy.getClassName(namePrefix, source.name, key, nameTestPredicate);
    }

    /**
     * Set the ClassLoader in which the class will be generated.
     * Concrete subclasses of AbstractClassGenerator (such as Enhancer)
     * will try to choose an appropriate default if this is unset.
     *

     * Classes are cached per-ClassLoader using a WeakHashMap, to allow
     * the generated classes to be removed when the associated loader is garbage collected.
     * @param classLoader the loader to generate the new class with, or null to use the default
     */
    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * Override the default naming policy.
     * @see DefaultNamingPolicy
     * @param namingPolicy the custom policy, or null to use the default
     */
    public void setNamingPolicy(NamingPolicy namingPolicy) {
        if (namingPolicy == null)
            namingPolicy = DefaultNamingPolicy.INSTANCE;
        this.namingPolicy = namingPolicy;
    }

    /**
     * @see #setNamingPolicy
     */
    public NamingPolicy getNamingPolicy() {
        return namingPolicy;
    }

    /**
     * Whether use and update the static cache of generated classes
     * for a class with the same properties. Default is true.
     */
    public void setUseCache(boolean useCache) {
        this.useCache = useCache;
    }

    /**
     * @see #setUseCache
     */
    public boolean getUseCache() {
        return useCache;
    }

    /**
     * If set, CGLIB will attempt to load classes from the specified
     * ClassLoader before generating them. Because generated
     * class names are not guaranteed to be unique, the default is false.
     */
    public void setAttemptLoad(boolean attemptLoad) {
        this.attemptLoad = attemptLoad;
    }

    public boolean getAttemptLoad() {
        return attemptLoad;
    }

    /**
     * Set the strategy to use to create the bytecode from this generator.
     * By default an instance of {@see DefaultGeneratorStrategy} is used.
     */
    public void setStrategy(GeneratorStrategy strategy) {
        if (strategy == null)
            strategy = DefaultGeneratorStrategy.INSTANCE;
        this.strategy = strategy;
    }

    /**
     * @see #setStrategy
     */
    public GeneratorStrategy getStrategy() {
        return strategy;
    }

    /**
     * Used internally by CGLIB. Returns the AbstractClassGenerator
     * that is being used to generate a class in the current thread.
     */
    public static AbstractClassGenerator getCurrent() {
        return (AbstractClassGenerator)CURRENT.get();
    }

    public ClassLoader getClassLoader() {
        ClassLoader t = classLoader;
        if (t == null) {
            t = getDefaultClassLoader();
        }
        if (t == null) {
            t = getClass().getClassLoader();
        }
        if (t == null) {
            t = Thread.currentThread().getContextClassLoader();
        }
        if (t == null) {
            throw new IllegalStateException("Cannot determine classloader");
        }
        return t;
    }

    abstract protected ClassLoader getDefaultClassLoader();

    /**
     * Returns the protection domain to use when defining the class.
     *

     * Default implementation returns null for using a default protection domain. Sub-classes may
     * override to use a more specific protection domain.
     *

     *
     * @return the protection domain (null for using a default)
     */
    protected ProtectionDomain getProtectionDomain() {
    	return null;
    }

    protected Object create(Object key) {
        try {
            ClassLoader loader = getClassLoader();
            Map<ClassLoader, ClassLoaderData> cache = CACHE;
            ClassLoaderData data = cache.get(loader);
            if (data == null) {
                synchronized (AbstractClassGenerator.class) {
                    cache = CACHE;
                    data = cache.get(loader);
                    if (data == null) {
                        Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
                        data = new ClassLoaderData(loader);
                        newCache.put(loader, data);
                        CACHE = newCache;
                    }
                }
            }
            this.key = key;
            Object obj = data.get(this, getUseCache());
            if (obj instanceof Class) {
                return firstInstance((Class) obj);
            }
            return nextInstance(obj);
        } catch (RuntimeException e) {
            throw e;
        } catch (Error e) {
            throw e;
        } catch (Exception e) {
            throw new CodeGenerationException(e);
        }
    }

    protected Class generate(ClassLoaderData data) {
        Class gen;
        Object save = CURRENT.get();
        CURRENT.set(this);
        try {
            ClassLoader classLoader = data.getClassLoader();
            if (classLoader == null) {
                throw new IllegalStateException("ClassLoader is null while trying to define class " +
                        getClassName() + ". It seems that the loader has been expired from a weak reference somehow. " +
                        "Please file an issue at cglib's issue tracker.");
            }
            synchronized (classLoader) {
              String name = generateClassName(data.getUniqueNamePredicate());
              data.reserveName(name);
              this.setClassName(name);
            }
            if (attemptLoad) {
                try {
                    gen = classLoader.loadClass(getClassName());
                    return gen;
                } catch (ClassNotFoundException e) {
                    // ignore
                }
            }
            byte[] b = strategy.generate(this);
            String className = ClassNameReader.getClassName(new ClassReader(b));
            ProtectionDomain protectionDomain = getProtectionDomain();
            synchronized (classLoader) { // just in case
                if (protectionDomain == null) {
                    gen = ReflectUtils.defineClass(className, b, classLoader);
                } else {
                    gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain);
                }
            }
            return gen;
        } catch (RuntimeException e) {
            throw e;
        } catch (Error e) {
            throw e;
        } catch (Exception e) {
            throw new CodeGenerationException(e);
        } finally {
            CURRENT.set(save);
        }
    }

    abstract protected Object firstInstance(Class type) throws Exception;
    abstract protected Object nextInstance(Object instance) throws Exception;
}


package net.sf.cglib.core.internal;

import java.util.concurrent.*;

public class LoadingCache<K, KK, V> {
    protected final ConcurrentMap<KK, Object> map;
    protected final Function<K, V> loader;
    protected final Function<K, KK> keyMapper;

    public static final Function IDENTITY = new Function() {
        public Object apply(Object key) {
            return key;
        }
    };

    public LoadingCache(Function<K, KK> keyMapper, Function<K, V> loader) {
        this.keyMapper = keyMapper;
        this.loader = loader;
        this.map = new ConcurrentHashMap<KK, Object>();
    }

    @SuppressWarnings("unchecked")
    public static <K> Function<K, K> identity() {
        return IDENTITY;
    }

    public V get(K key) {
        final KK cacheKey = keyMapper.apply(key);
        Object v = map.get(cacheKey);
        if (v != null && !(v instanceof FutureTask)) {
            return (V) v;
        }

        return createEntry(key, cacheKey, v);
    }

    /**
     * Loads entry to the cache.
     * If entry is missing, put {@link FutureTask} first so other competing thread might wait for the result.
     * @param key original key that would be used to load the instance
     * @param cacheKey key that would be used to store the entry in internal map
     * @param v null or {@link FutureTask<V>}
     * @return newly created instance
     */
    protected V createEntry(final K key, KK cacheKey, Object v) {
        FutureTask<V> task;
        boolean creator = false;
        if (v != null) {
            // Another thread is already loading an instance
            task = (FutureTask<V>) v;
        } else {
            task = new FutureTask<V>(new Callable<V>() {
                public V call() throws Exception {
                    return loader.apply(key);
                }
            });
            Object prevTask = map.putIfAbsent(cacheKey, task);
            if (prevTask == null) {
                // creator does the load
                creator = true;
                task.run();
            } else if (prevTask instanceof FutureTask) {
                task = (FutureTask<V>) prevTask;
            } else {
                return (V) prevTask;
            }
        }

        V result;
        try {
            result = task.get();
        } catch (InterruptedException e) {
            throw new IllegalStateException("Interrupted while loading cache item", e);
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw ((RuntimeException) cause);
            }
            throw new IllegalStateException("Unable to load cache item", cause);
        }
        if (creator) {
            map.put(cacheKey, result);
        }
        return result;
    }
}


/*
 * Copyright 2003,2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.cglib.core;

import java.beans.*;
import java.lang.reflect.*;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.util.*;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.Type;

/**
 * @version $Id: ReflectUtils.java,v 1.30 2009/01/11 19:47:49 herbyderby Exp $
 */
public class ReflectUtils {
    private ReflectUtils() { }

    private static final Map primitives = new HashMap(8);
    private static final Map transforms = new HashMap(8);
    private static final ClassLoader defaultLoader = ReflectUtils.class.getClassLoader();
    private static Method DEFINE_CLASS, DEFINE_CLASS_UNSAFE;
    private static final ProtectionDomain PROTECTION_DOMAIN;
    private static final Object UNSAFE;
    private static final Throwable THROWABLE;

    private static final List<Method> OBJECT_METHODS = new ArrayList<Method>();

    static {
        ProtectionDomain protectionDomain;
        Method defineClass, defineClassUnsafe;
        Object unsafe;
        Throwable throwable = null;
        try {
            protectionDomain = getProtectionDomain(ReflectUtils.class);
            try {
                defineClass = (Method) AccessController.doPrivileged(new PrivilegedExceptionAction() {
                    public Object run() throws Exception {
                            Class loader = Class.forName("java.lang.ClassLoader"); // JVM crash w/o this
                            Method defineClass = loader.getDeclaredMethod("defineClass",
                                                                    new Class[]{ String.class,
                                                                                byte[].class,
                                                                                Integer.TYPE,
                                                                                Integer.TYPE,
                                                                                ProtectionDomain.class });
                            defineClass.setAccessible(true);
                            return defineClass;
                    }
                });
                defineClassUnsafe = null;
                unsafe = null;
            } catch (Throwable t) {
                // Fallback on Jigsaw where this method is not available.
                throwable = t;
                defineClass = null;
                unsafe = AccessController.doPrivileged(new PrivilegedExceptionAction() {
                    public Object run() throws Exception {
                        Class u = Class.forName("sun.misc.Unsafe");
                        Field theUnsafe = u.getDeclaredField("theUnsafe");
                        theUnsafe.setAccessible(true);
                        return theUnsafe.get(null);
                    }
                });
                Class u = Class.forName("sun.misc.Unsafe");
                defineClassUnsafe = u.getMethod("defineClass",
                                        new Class[]{ String.class,
                                                     byte[].class,
                                                     Integer.TYPE,
                                                     Integer.TYPE,
                                                     ClassLoader.class,
                                                     ProtectionDomain.class });
            }
            AccessController.doPrivileged(new PrivilegedExceptionAction() {
                public Object run() throws Exception {
                    Method[] methods = Object.class.getDeclaredMethods();
                    for (Method method : methods) {
                        if ("finalize".equals(method.getName())
                                || (method.getModifiers() & (Modifier.FINAL | Modifier.STATIC)) > 0) {
                            continue;
                        }
                        OBJECT_METHODS.add(method);
                    }
                    return null;
                }
            });
        } catch (Throwable t) {
            if (throwable == null) {
                throwable = t;
            }
            protectionDomain = null;
            defineClass = null;
            defineClassUnsafe = null;
            unsafe = null;
        }
        PROTECTION_DOMAIN = protectionDomain;
        DEFINE_CLASS = defineClass;
        DEFINE_CLASS_UNSAFE = defineClassUnsafe;
        UNSAFE = unsafe;
        THROWABLE = throwable;
    }

    private static final String[] CGLIB_PACKAGES = {
        "java.lang",
    };

    static {
        primitives.put("byte", Byte.TYPE);
        primitives.put("char", Character.TYPE);
        primitives.put("double", Double.TYPE);
        primitives.put("float", Float.TYPE);
        primitives.put("int", Integer.TYPE);
        primitives.put("long", Long.TYPE);
        primitives.put("short", Short.TYPE);
        primitives.put("boolean", Boolean.TYPE);

        transforms.put("byte", "B");
        transforms.put("char", "C");
        transforms.put("double", "D");
        transforms.put("float", "F");
        transforms.put("int", "I");
        transforms.put("long", "J");
        transforms.put("short", "S");
        transforms.put("boolean", "Z");
    }

    public static ProtectionDomain getProtectionDomain(final Class source) {
    	if(source == null) {
    		return null;
    	}
        return (ProtectionDomain)AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return source.getProtectionDomain();
            }
        });
    }

    public static Type[] getExceptionTypes(Member member) {
        if (member instanceof Method) {
            return TypeUtils.getTypes(((Method)member).getExceptionTypes());
        } else if (member instanceof Constructor) {
            return TypeUtils.getTypes(((Constructor)member).getExceptionTypes());
        } else {
            throw new IllegalArgumentException("Cannot get exception types of a field");
        }
    }

    public static Signature getSignature(Member member) {
        if (member instanceof Method) {
            return new Signature(member.getName(), Type.getMethodDescriptor((Method)member));
        } else if (member instanceof Constructor) {
            Type[] types = TypeUtils.getTypes(((Constructor)member).getParameterTypes());
            return new Signature(Constants.CONSTRUCTOR_NAME,
                                 Type.getMethodDescriptor(Type.VOID_TYPE, types));

        } else {
            throw new IllegalArgumentException("Cannot get signature of a field");
        }
    }

    public static Constructor findConstructor(String desc) {
        return findConstructor(desc, defaultLoader);
    }

    public static Constructor findConstructor(String desc, ClassLoader loader) {
        try {
            int lparen = desc.indexOf('(');
            String className = desc.substring(0, lparen).trim();
            return getClass(className, loader).getConstructor(parseTypes(desc, loader));
        } catch (ClassNotFoundException e) {
            throw new CodeGenerationException(e);
        } catch (NoSuchMethodException e) {
            throw new CodeGenerationException(e);
        }
    }

    public static Method findMethod(String desc) {
        return findMethod(desc, defaultLoader);
    }

    public static Method findMethod(String desc, ClassLoader loader) {
        try {
            int lparen = desc.indexOf('(');
            int dot = desc.lastIndexOf('.', lparen);
            String className = desc.substring(0, dot).trim();
            String methodName = desc.substring(dot + 1, lparen).trim();
            return getClass(className, loader).getDeclaredMethod(methodName, parseTypes(desc, loader));
        } catch (ClassNotFoundException e) {
            throw new CodeGenerationException(e);
        } catch (NoSuchMethodException e) {
            throw new CodeGenerationException(e);
        }
    }

    private static Class[] parseTypes(String desc, ClassLoader loader) throws ClassNotFoundException {
        int lparen = desc.indexOf('(');
        int rparen = desc.indexOf(')', lparen);
        List params = new ArrayList();
        int start = lparen + 1;
        for (;;) {
            int comma = desc.indexOf(',', start);
            if (comma < 0) {
                break;
            }
            params.add(desc.substring(start, comma).trim());
            start = comma + 1;
        }
        if (start < rparen) {
            params.add(desc.substring(start, rparen).trim());
        }
        Class[] types = new Class[params.size()];
        for (int i = 0; i < types.length; i++) {
            types[i] = getClass((String)params.get(i), loader);
        }
        return types;
    }

    private static Class getClass(String className, ClassLoader loader) throws ClassNotFoundException {
        return getClass(className, loader, CGLIB_PACKAGES);
    }

    private static Class getClass(String className, ClassLoader loader, String[] packages) throws ClassNotFoundException {
        String save = className;
        int dimensions = 0;
        int index = 0;
        while ((index = className.indexOf("[]", index) + 1) > 0) {
            dimensions++;
        }
        StringBuffer brackets = new StringBuffer(className.length() - dimensions);
        for (int i = 0; i < dimensions; i++) {
            brackets.append('[');
        }
        className = className.substring(0, className.length() - 2 * dimensions);

        String prefix = (dimensions > 0) ? brackets + "L" : "";
        String suffix = (dimensions > 0) ? ";" : "";
        try {
            return Class.forName(prefix + className + suffix, false, loader);
        } catch (ClassNotFoundException ignore) { }
        for (int i = 0; i < packages.length; i++) {
            try {
                return Class.forName(prefix + packages[i] + '.' + className + suffix, false, loader);
            } catch (ClassNotFoundException ignore) { }
        }
        if (dimensions == 0) {
            Class c = (Class)primitives.get(className);
            if (c != null) {
                return c;
            }
        } else {
            String transform = (String)transforms.get(className);
            if (transform != null) {
                try {
                    return Class.forName(brackets + transform, false, loader);
                } catch (ClassNotFoundException ignore) { }
            }
        }
        throw new ClassNotFoundException(save);
    }

    public static Object newInstance(Class type) {
        return newInstance(type, Constants.EMPTY_CLASS_ARRAY, null);
    }

    public static Object newInstance(Class type, Class[] parameterTypes, Object[] args) {
        return newInstance(getConstructor(type, parameterTypes), args);
    }

    public static Object newInstance(final Constructor cstruct, final Object[] args) {

        boolean flag = cstruct.isAccessible();
        try {
            if (!flag) {
                cstruct.setAccessible(true);
            }
            Object result = cstruct.newInstance(args);
            return result;
        } catch (InstantiationException e) {
            throw new CodeGenerationException(e);
        } catch (IllegalAccessException e) {
            throw new CodeGenerationException(e);
        } catch (InvocationTargetException e) {
            throw new CodeGenerationException(e.getTargetException());
        } finally {
            if (!flag) {
                cstruct.setAccessible(flag);
            }
        }

    }

    public static Constructor getConstructor(Class type, Class[] parameterTypes) {
        try {
            Constructor constructor = type.getDeclaredConstructor(parameterTypes);
            constructor.setAccessible(true);
            return constructor;
        } catch (NoSuchMethodException e) {
            throw new CodeGenerationException(e);
        }
    }

    public static String[] getNames(Class[] classes)
    {
        if (classes == null)
            return null;
        String[] names = new String[classes.length];
        for (int i = 0; i < names.length; i++) {
            names[i] = classes[i].getName();
        }
        return names;
    }

    public static Class[] getClasses(Object[] objects) {
        Class[] classes = new Class[objects.length];
        for (int i = 0; i < objects.length; i++) {
            classes[i] = objects[i].getClass();
        }
        return classes;
    }

    public static Method findNewInstance(Class iface) {
        Method m = findInterfaceMethod(iface);
        if (!m.getName().equals("newInstance")) {
            throw new IllegalArgumentException(iface + " missing newInstance method");
        }
        return m;
    }

    public static Method[] getPropertyMethods(PropertyDescriptor[] properties, boolean read, boolean write) {
        Set methods = new HashSet();
        for (int i = 0; i < properties.length; i++) {
            PropertyDescriptor pd = properties[i];
            if (read) {
                methods.add(pd.getReadMethod());
            }
            if (write) {
                methods.add(pd.getWriteMethod());
            }
        }
        methods.remove(null);
        return (Method[])methods.toArray(new Method[methods.size()]);
    }

    public static PropertyDescriptor[] getBeanProperties(Class type) {
        return getPropertiesHelper(type, true, true);
    }

    public static PropertyDescriptor[] getBeanGetters(Class type) {
        return getPropertiesHelper(type, true, false);
    }

    public static PropertyDescriptor[] getBeanSetters(Class type) {
        return getPropertiesHelper(type, false, true);
    }

    private static PropertyDescriptor[] getPropertiesHelper(Class type, boolean read, boolean write) {
        try {
            BeanInfo info = Introspector.getBeanInfo(type, Object.class);
            PropertyDescriptor[] all = info.getPropertyDescriptors();
            if (read && write) {
                return all;
            }
            List properties = new ArrayList(all.length);
            for (int i = 0; i < all.length; i++) {
                PropertyDescriptor pd = all[i];
                if ((read && pd.getReadMethod() != null) ||
                    (write && pd.getWriteMethod() != null)) {
                    properties.add(pd);
                }
            }
            return (PropertyDescriptor[])properties.toArray(new PropertyDescriptor[properties.size()]);
        } catch (IntrospectionException e) {
            throw new CodeGenerationException(e);
        }
    }

    public static Method findDeclaredMethod(final Class type,
                                            final String methodName, final Class[] parameterTypes)
    throws NoSuchMethodException {

        Class cl = type;
        while (cl != null) {
            try {
                return cl.getDeclaredMethod(methodName, parameterTypes);
            } catch (NoSuchMethodException e) {
                cl = cl.getSuperclass();
            }
        }
        throw new NoSuchMethodException(methodName);

    }

    public static List addAllMethods(final Class type, final List list) {

        if (type == Object.class) {
            list.addAll(OBJECT_METHODS);
        } else
            list.addAll(java.util.Arrays.asList(type.getDeclaredMethods()));

        Class superclass = type.getSuperclass();
        if (superclass != null) {
            addAllMethods(superclass, list);
        }
        Class[] interfaces = type.getInterfaces();
        for (int i = 0; i < interfaces.length; i++) {
            addAllMethods(interfaces[i], list);
        }

        return list;
    }

    public static List addAllInterfaces(Class type, List list) {
        Class superclass = type.getSuperclass();
        if (superclass != null) {
            list.addAll(Arrays.asList(type.getInterfaces()));
            addAllInterfaces(superclass, list);
        }
        return list;
    }

    public static Method findInterfaceMethod(Class iface) {
        if (!iface.isInterface()) {
            throw new IllegalArgumentException(iface + " is not an interface");
        }
        Method[] methods = iface.getDeclaredMethods();
        if (methods.length != 1) {
            throw new IllegalArgumentException("expecting exactly 1 method in " + iface);
        }
        return methods[0];
    }

    public static Class defineClass(String className, byte[] b, ClassLoader loader) throws Exception {
        return defineClass(className, b, loader, PROTECTION_DOMAIN);
    }

    public static Class defineClass(String className, byte[] b, ClassLoader loader, ProtectionDomain protectionDomain) throws Exception {
        Class c;
        if (DEFINE_CLASS != null) {
            Object[] args = new Object[]{className, b, new Integer(0), new Integer(b.length), protectionDomain };
            c = (Class)DEFINE_CLASS.invoke(loader, args);
        } else if (DEFINE_CLASS_UNSAFE != null) {
            Object[] args = new Object[]{className, b, new Integer(0), new Integer(b.length), loader, protectionDomain };
            c = (Class)DEFINE_CLASS_UNSAFE.invoke(UNSAFE, args);
        } else {
            throw new CodeGenerationException(THROWABLE);
        }
        // Force static initializers to run.
        Class.forName(className, true, loader);
        return c;
    }

    public static int findPackageProtected(Class[] classes) {
        for (int i = 0; i < classes.length; i++) {
            if (!Modifier.isPublic(classes[i].getModifiers())) {
                return i;
            }
        }
        return 0;
    }

    public static MethodInfo getMethodInfo(final Member member, final int modifiers) {
        final Signature sig = getSignature(member);
        return new MethodInfo() {
            private ClassInfo ci;
            public ClassInfo getClassInfo() {
                if (ci == null)
                    ci = ReflectUtils.getClassInfo(member.getDeclaringClass());
                return ci;
            }
            public int getModifiers() {
                return modifiers;
            }
            public Signature getSignature() {
                return sig;
            }
            public Type[] getExceptionTypes() {
                return ReflectUtils.getExceptionTypes(member);
            }
            public Attribute getAttribute() {
                return null;
            }
        };
    }

    public static MethodInfo getMethodInfo(Member member) {
        return getMethodInfo(member, member.getModifiers());
    }

    public static ClassInfo getClassInfo(final Class clazz) {
        final Type type = Type.getType(clazz);
        final Type sc = (clazz.getSuperclass() == null) ? null : Type.getType(clazz.getSuperclass());
        return new ClassInfo() {
            public Type getType() {
                return type;
            }
            public Type getSuperType() {
                return sc;
            }
            public Type[] getInterfaces() {
                return TypeUtils.getTypes(clazz.getInterfaces());
            }
            public int getModifiers() {
                return clazz.getModifiers();
            }
        };
    }

    // used by MethodInterceptorGenerated generated code
    public static Method[] findMethods(String[] namesAndDescriptors, Method[] methods)
    {
        Map map = new HashMap();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            map.put(method.getName() + Type.getMethodDescriptor(method), method);
        }
        Method[] result = new Method[namesAndDescriptors.length / 2];
        for (int i = 0; i < result.length; i++) {
            result[i] = (Method)map.get(namesAndDescriptors[i * 2] + namesAndDescriptors[i * 2 + 1]);
            if (result[i] == null) {
                // TODO: error?
            }
        }
        return result;
    }
}


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

package jodd.io;

import jodd.util.StringUtil;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

public class PathUtil {

	/**
	 * Resolves subpath in safer way. For some reason, if child starts with
	 * a separator it gets resolved as a full path, ignoring the base.
	 * This method acts different.
	 */
	public static Path resolve(final Path base, String child) {
		if (StringUtil.startsWithChar(child, File.separatorChar)) {
			child = child.substring(1);
		}
		return base.resolve(child);
	}

	public static Path resolve(Path path, final String... childs) {
		for (String child : childs) {
			path = resolve(path, child);
		}
		return path;
	}

	/**
	 * Reads path content.
	 */
	public static String readString(final Path path) throws IOException {
		try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
			StringWriter writer = new StringWriter(); // flush & close not needed for StringWriter-instance
			StreamUtil.copy(reader, writer);
			return writer.toString();
		}
	}

	/**
	 * Deletes a directory recursively.
	 */
	public static void deleteFileTree(final Path directory) throws IOException {
		Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
				Files.delete(file);
				return FileVisitResult.CONTINUE;
			}

			@Override
			public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
				Files.delete(dir);
				return FileVisitResult.CONTINUE;
			}
		});
	}

}


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

package jodd.http;

import jodd.net.HttpMethod;
import jodd.net.MimeTypes;
import jodd.util.Base64;
import jodd.util.StringBand;
import jodd.util.StringPool;
import jodd.util.StringUtil;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;

import static jodd.util.StringPool.CRLF;
import static jodd.util.StringPool.SPACE;

/**
 * HTTP request.
 */
public class HttpRequest extends HttpBase<HttpRequest> {

	protected String protocol = "http";
	protected String host = "localhost";
	protected int port = Defaults.DEFAULT_PORT;
	protected String method = "GET";
	protected String path = StringPool.SLASH;
	protected HttpMultiMap<String> query;

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

	public HttpRequest() {
		initRequest();
	}

	/**
	 * Prepares request on creation. By default, it just
	 * adds "Connection: Close" header.
	 */
	protected void initRequest() {
		connectionKeepAlive(false);
	}

	// ---------------------------------------------------------------- properties

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

	/**
	 * Sets request host name.
	 */
	public HttpRequest host(final String host) {
		this.host = host;
		if (headers.contains(HEADER_HOST)) {
			headerOverwrite(HEADER_HOST, host);
		}
		return this;
	}

	/**
	 * Returns used protocol. By default it's "http".
	 */
	public String protocol() {
		return protocol;
	}

	/**
	 * Defines protocol.
	 */
	public HttpRequest protocol(final String protocol) {
		this.protocol = protocol;
		return this;
	}

	/**
	 * Returns request port number. When port is not
	 * explicitly defined, returns default port for
	 * current protocol.
	 */
	public int port() {
		if (port == Defaults.DEFAULT_PORT) {
			if (protocol == null) {
				return 80;
			}
			if (protocol.equalsIgnoreCase("https")) {
				return 443;
			}
			return 80;
		}
		return port;
	}

	/**
	 * Sets request port number.
	 */
	public HttpRequest port(final int port) {
		this.port = port;
		return this;
	}

	// ---------------------------------------------------------------- set

	/**
	 * Sets the destination (method, host, port... ) at once.
	 */
	public HttpRequest set(String destination) {
		destination = destination.trim();

		// http method, optional

		int ndx = destination.indexOf(' ');

		if (ndx != -1) {
			String method = destination.substring(0, ndx).toUpperCase();

			try {
				HttpMethod httpMethod = HttpMethod.valueOf(method);
				this.method = httpMethod.name();
				destination = destination.substring(ndx + 1);
			}
			catch (IllegalArgumentException ignore) {
				// unknown http method
			}
		}

		// protocol

		ndx = destination.indexOf("://");

		if (ndx != -1) {
			protocol = destination.substring(0, ndx);
			destination = destination.substring(ndx + 3);
		}

		// host

		ndx = destination.indexOf('/');

		if (ndx == -1) {
			ndx = destination.length();
		}

		if (ndx != 0) {

			String hostToSet = destination.substring(0, ndx);
			destination = destination.substring(ndx);

			// port

			ndx = hostToSet.indexOf(':');

			if (ndx == -1) {
				port = Defaults.DEFAULT_PORT;
			} else {
				port = Integer.parseInt(hostToSet.substring(ndx + 1));
				hostToSet = hostToSet.substring(0, ndx);
			}

			host(hostToSet);
		}

		// path + query

		path(destination);

		return this;
	}

	// ---------------------------------------------------------------- static factories

	/**
	 * Generic request builder, usually used when method is a variable.
	 * Otherwise, use one of the other static request builder methods.
	 */
	public static HttpRequest create(final String method, final String destination) {
		return new HttpRequest()
				.method(method.toUpperCase())
				.set(destination);
	}

	/**
	 * Builds a CONNECT request.
	 */
	public static HttpRequest connect(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.CONNECT)
				.set(destination);
	}
	/**
	 * Builds a GET request.
	 */
	public static HttpRequest get(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.GET)
				.set(destination);
	}
	/**
	 * Builds a POST request.
	 */
	public static HttpRequest post(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.POST)
				.set(destination);
	}
	/**
	 * Builds a PUT request.
	 */
	public static HttpRequest put(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.PUT)
				.set(destination);
	}
	/**
	 * Builds a PATCH request.
	 */
	public static HttpRequest patch(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.PATCH)
				.set(destination);
	}
	/**
	 * Builds a DELETE request.
	 */
	public static HttpRequest delete(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.DELETE)
				.set(destination);
	}
	/**
	 * Builds a HEAD request.
	 */
	public static HttpRequest head(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.HEAD)
				.set(destination);
	}
	/**
	 * Builds a TRACE request.
	 */
	public static HttpRequest trace(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.TRACE)
				.set(destination);
	}
	/**
	 * Builds an OPTIONS request.
	 */
	public static HttpRequest options(final String destination) {
		return new HttpRequest()
				.method(HttpMethod.OPTIONS)
				.set(destination);
	}

	// ---------------------------------------------------------------- request

	/**
	 * Returns request method.
	 */
	public String method() {
		return method;
	}

	/**
	 * Specifies request method. It will be converted into uppercase.
	 * Does not validate if method is one of the HTTP methods.
	 */
	public HttpRequest method(final String method) {
		this.method = method.toUpperCase();
		return this;
	}
	public HttpRequest method(final HttpMethod httpMethod) {
		this.method = httpMethod.name();
		return this;
	}

	/**
	 * Returns request path, without the query.
	 */
	public String path() {
		return path;
	}

	/**
	 * Sets request path. Query string is allowed.
	 * Adds a slash if path doesn't start with one.
	 * Query will be stripped out from the path.
	 * Previous query is discarded.
	 * @see #query()
	 */
	public HttpRequest path(String path) {
		// this must be the only place that sets the path

		if (!path.startsWith(StringPool.SLASH)) {
			path = StringPool.SLASH + path;
		}

		int ndx = path.indexOf('?');

		if (ndx != -1) {
			String queryString = path.substring(ndx + 1);

			path = path.substring(0, ndx);

			query = HttpUtil.parseQuery(queryString, true);
		} else {
			query = HttpMultiMap.newCaseInsensitiveMap();
		}

		this.path = path;

		return this;
	}

	/**
	 * Forces multipart requests. When set to false,
	 * it will be {@link #isFormMultipart() detected} if request
	 * should be multipart. By setting this to true
	 * we are forcing usage of multipart request.
	 */
	public HttpRequest multipart(final boolean multipart) {
		this.multipart = multipart;
		return this;
	}

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

	/**
	 * Sets cookies to the request.
	 */
	public HttpRequest cookies(final Cookie... cookies) {
		if (cookies.length == 0) {
			return this;
		}

		StringBuilder cookieString = new StringBuilder();

		boolean first = true;

		for (Cookie cookie : cookies) {
			Integer maxAge = cookie.getMaxAge();
			if (maxAge != null && maxAge.intValue() == 0) {
				continue;
			}

			if (!first) {
				cookieString.append("; ");
			}

			first = false;
			cookieString.append(cookie.getName());
			cookieString.append('=');
			cookieString.append(cookie.getValue());
		}

		headerOverwrite("cookie", cookieString.toString());

		return this;
	}

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

	/**
	 * Adds query parameter.
	 */
	public HttpRequest query(final String name, final String value) {
		query.add(name, value);
		return this;
	}

	/**
	 * Adds many query parameters at once. Although it accepts objects,
	 * each value will be converted to string.
	 */
	public HttpRequest query(final String name1, final Object value1, final Object... parameters) {
		query(name1, value1 == null ? null : value1.toString());

		for (int i = 0; i < parameters.length; i += 2) {
			String name = parameters[i].toString();

			String value = parameters[i + 1].toString();
			query.add(name, value);
		}
		return this;
	}

	/**
	 * Adds all parameters from the provided map.
	 */
	public HttpRequest query(final Map<String, String> queryMap) {
		for (Map.Entry<String, String> entry : queryMap.entrySet()) {
			query.add(entry.getKey(), entry.getValue());
		}
		return this;
	}

	/**
	 * Returns backend map of query parameters.
	 */
	public HttpMultiMap<String> query() {
		return query;
	}

	/**
	 * Clears all query parameters.
	 */
	public HttpRequest clearQueries() {
		query.clear();
		return this;
	}

	/**
	 * Removes query parameters for given name.
	 */
	public HttpRequest queryRemove(final String name) {
		query.remove(name);
		return this;
	}

	// ---------------------------------------------------------------- queryString

	/**
	 * @see #queryString(String, boolean)
	 */
	public HttpRequest queryString(final String queryString) {
		return queryString(queryString, true);
	}

	/**
	 * Sets query from provided query string. Previous query values
	 * are discarded.
	 */
	public HttpRequest queryString(final String queryString, final boolean decode) {
		this.query = HttpUtil.parseQuery(queryString, decode);
		return this;
	}

	/**
	 * Generates query string. All values are URL encoded.
	 */
	public String queryString() {
		if (query == null) {
			return StringPool.EMPTY;
		}
		return HttpUtil.buildQuery(query, queryEncoding);
	}

	// ---------------------------------------------------------------- query encoding

	protected String queryEncoding = Defaults.queryEncoding;

	/**
	 * Defines encoding for query parameters.
	 */
	public HttpRequest queryEncoding(final String encoding) {
		this.queryEncoding = encoding;
		return this;
	}

	// ---------------------------------------------------------------- full path

	/**
	 * Returns full URL path.
	 * Simply concatenates {@link #protocol(String) protocol}, {@link #host(String) host},
	 * {@link #port(int) port}, {@link #path(String) path} and {@link #queryString(String) query string}.
	 */
	public String url() {
		StringBuilder url = new StringBuilder();

		url.append(hostUrl());

		if (path != null) {
			url.append(path);
		}

		String queryString = queryString();

		if (StringUtil.isNotBlank(queryString)) {
			url.append('?');
			url.append(queryString);
		}

		return url.toString();
	}

	/**
	 * Returns just host url, without path and query.
	 */
	public String hostUrl() {
		StringBand url = new StringBand(8);

		if (protocol != null) {
			url.append(protocol);
			url.append("://");
		}

		if (host != null) {
			url.append(host);
		}

		if (port != Defaults.DEFAULT_PORT) {
			url.append(':');
			url.append(port);
		}

		return url.toString();
	}

	// ---------------------------------------------------------------- auth

	/**
	 * Enables basic authentication by adding required header.
	 */
	public HttpRequest basicAuthentication(final String username, final String password) {
		if (username != null && password != null) {
			String data = username.concat(StringPool.COLON).concat(password);

			String base64 = Base64.encodeToString(data);

			headerOverwrite(HEADER_AUTHORIZATION, "Basic " + base64);
		}

		return this;
	}

	/**
	 * Enables token-based authentication.
	 */
	public HttpRequest tokenAuthentication(final String token) {
		if (token != null) {
			headerOverwrite(HEADER_AUTHORIZATION, "Bearer " + token);
		}
		return this;
	}

	// ---------------------------------------------------------------- https

	private boolean trustAllCertificates;
	private boolean verifyHttpsHost = true;

	/**
	 * Trusts all certificates, use with caution.
	 */
	public HttpRequest trustAllCerts(final boolean trust) {
		trustAllCertificates = trust;
		return this;
	}

	/**
	 * Returns a flag if to trusts all certificates.
	 */
	public boolean trustAllCertificates() {
		return trustAllCertificates;
	}

	/**
	 * Verifies HTTPS hosts.
	 */
	public HttpRequest verifyHttpsHost(final boolean verifyHttpsHost) {
		this.verifyHttpsHost = verifyHttpsHost;
		return this;
	}

	/**
	 * Returns a flag if to verify https hosts.
	 */
	public boolean verifyHttpsHost() {
		return verifyHttpsHost;
	}

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

	/**
	 * Sets 'Host' header from current host and port.
	 */
	public HttpRequest setHostHeader() {
		String hostPort = this.host;

		if (port != Defaults.DEFAULT_PORT) {
			hostPort += StringPool.COLON + port;
		}

		headerOverwrite(HEADER_HOST, hostPort);
		return this;
	}

	// ---------------------------------------------------------------- monitor

	/**
	 * Registers {@link jodd.http.HttpProgressListener listener} that will
	 * monitor upload progress. Be aware that the whole size of the
	 * request is being monitored, not only the files content.
	 */
	public HttpRequest monitor(final HttpProgressListener httpProgressListener) {
		this.httpProgressListener = httpProgressListener;
		return this;
	}

	// ---------------------------------------------------------------- connection properties

	protected int timeout = -1;
	protected int connectTimeout = -1;
	protected boolean followRedirects = false;
	protected int maxRedirects = 50;

	/**
	 * Defines the socket timeout (SO_TIMEOUT) in milliseconds, which is the timeout for waiting for data or,
	 * put differently, a maximum period inactivity between two consecutive data packets).
	 * After establishing the connection, the client socket waits for response after sending
	 * the request. This is the elapsed time since the client has sent request to the
	 * server before server responds. Please note that this is not same as HTTP Error 408 which
	 * the server sends to the client. In other words its maximum period inactivity between
	 * two consecutive data packets arriving at client side after connection is established.
	 * A timeout value of zero is interpreted as an infinite timeout.
	 * @see jodd.http.HttpConnection#setTimeout(int)
	 */
	public HttpRequest timeout(final int milliseconds) {
		this.timeout = milliseconds;
		return this;
	}

	/**
	 * Returns read timeout (SO_TIMEOUT) in milliseconds. Negative value
	 * means that default value is used.
	 * @see #timeout(int)
	 */
	public int timeout() {
		return timeout;
	}

	/**
	 * Defines the socket timeout (SO_TIMEOUT) in milliseconds, which is the timeout
	 * for waiting for data or, put differently, a maximum period inactivity between
	 * two consecutive data packets). A timeout value of zero is interpreted as
	 * an infinite timeout.
	 */
	public HttpRequest connectionTimeout(final int milliseconds) {
		this.connectTimeout = milliseconds;
		return this;
	}

	/**
	 * Returns socket connection timeout. Negative value means that default
	 * value is used.
	 * @see #connectionTimeout(int)
	 */
	public int connectionTimeout() {
		return connectTimeout;
	}

	/**
	 * Defines if redirects responses should be followed. NOTE: when redirection is enabled,
	 * the original URL will NOT be preserved in the request!
	 */
	public HttpRequest followRedirects(final boolean followRedirects) {
		this.followRedirects = followRedirects;
		return this;
	}

	/**
	 * Returns {@code true} if redirects are followed.
	 */
	public boolean isFollowRedirects() {
		return this.followRedirects;
	}

	/**
	 * Sets the max number of redirects, used when {@link #followRedirects} is enabled.
	 */
	public HttpRequest maxRedirects(final int maxRedirects) {
		this.maxRedirects = maxRedirects;
		return this;
	}

	/**
	 * Returns max number of redirects, used when {@link #followRedirects} is enabled.
	 */
	public int maxRedirects() {
		return this.maxRedirects;
	}

	// ---------------------------------------------------------------- send

	protected HttpConnection httpConnection;
	protected HttpConnectionProvider httpConnectionProvider;

	/**
	 * Uses custom connection provider when {@link #open() opening} the
	 * connection.
	 */
	public HttpRequest withConnectionProvider(final HttpConnectionProvider httpConnectionProvider) {
		this.httpConnectionProvider = httpConnectionProvider;
		return this;
	}

	/**
	 * Returns http connection provider that was used for creating
	 * current http connection. If null, default
	 * connection provider will be used.
	 */
	public HttpConnectionProvider connectionProvider() {
		return httpConnectionProvider;
	}

	/**
	 * Returns {@link HttpConnection} that is going to be
	 * used for sending this request. Value is available
	 * ONLY after calling {@link #open()} and before {@link #send()}.
	 */
	public HttpConnection connection() {
		return httpConnection;
	}

	/**
	 * Opens a new {@link HttpConnection connection} using either
	 * provided or {@link HttpConnectionProvider default} connection
	 * provider.
	 */
	public HttpRequest open() {
		if (httpConnectionProvider == null) {
			return open(HttpConnectionProvider.get());
		}

		return open(httpConnectionProvider);
	}

	/**
	 * Opens a new {@link jodd.http.HttpConnection connection}
	 * using given {@link jodd.http.HttpConnectionProvider}.
	 */
	public HttpRequest open(final HttpConnectionProvider httpConnectionProvider) {
		if (this.httpConnection != null) {
			throw new HttpException("Connection already opened");
		}
		try {
			this.httpConnectionProvider = httpConnectionProvider;
			this.httpConnection = httpConnectionProvider.createHttpConnection(this);
		} catch (IOException ioex) {
			throw new HttpException("Can't connect to: " + url(), ioex);
		}

		return this;
	}

	/**
	 * Assignees provided {@link jodd.http.HttpConnection} for communication.
	 * It does not actually opens it until the {@link #send() sending}.
	 */
	public HttpRequest open(final HttpConnection httpConnection) {
		if (this.httpConnection != null) {
			throw new HttpException("Connection already opened");
		}
		this.httpConnection = httpConnection;
		this.httpConnectionProvider = null;
		return this;
	}

	/**
	 * Continues using the same keep-alive connection.
	 * Don't use any variant of open() when
	 * continuing the communication!
	 * First it checks if "Connection" header exist in the response
	 * and if it is equal to "Keep-Alive" value. Then it
	 * checks the "Keep-Alive" headers "max" parameter.
	 * If its value is positive, then the existing {@link jodd.http.HttpConnection}
	 * from the request will be reused. If max value is 1,
	 * connection will be sent with "Connection: Close" header, indicating
	 * its the last request. When new connection is created, the
	 * same {@link jodd.http.HttpConnectionProvider} that was used for
	 * creating initial connection is used for opening the new connection.
	 *
	 * @param doContinue set it to false to indicate the last connection
	 */
	public HttpRequest keepAlive(final HttpResponse httpResponse, final boolean doContinue) {
		boolean keepAlive = httpResponse.isConnectionPersistent();
		if (keepAlive) {
			HttpConnection previousConnection = httpResponse.getHttpRequest().httpConnection;

			if (previousConnection != null) {
				// keep using the connection!
				this.httpConnection = previousConnection;
				this.httpConnectionProvider = httpResponse.getHttpRequest().connectionProvider();
			}

			//keepAlive = true; (already set)
		} else {
			// close previous connection
			httpResponse.close();

			// force keep-alive on new request
			keepAlive = true;
		}

		// if we don't want to continue with this persistent session, mark this connection as closed
		if (!doContinue) {
			keepAlive = false;
		}

		connectionKeepAlive(keepAlive);

		// if connection is not opened, open it using previous connection provider
		if (httpConnection == null) {
			open(httpResponse.getHttpRequest().connectionProvider());
		}
		return this;
	}

	/**
	 * {@link #open() Opens connection} if not already open, sends request,
	 * reads response and closes the request. If keep-alive mode is enabled
	 * connection will not be closed.
	 */
	public HttpResponse send() {
		if (!followRedirects) {
			return _send();
		}

		int redirects = this.maxRedirects;

		while (redirects > 0) {
			redirects--;

			final HttpResponse httpResponse = _send();

			final int statusCode = httpResponse.statusCode();

			if (HttpStatus.isRedirect(statusCode)) {
				_reset();
				set(httpResponse.location());
				continue;
			}

			return httpResponse;
		}

		throw new HttpException("Max number of redirects exceeded: " + this.maxRedirects);
	}

	/**
	 * Resets the request by resetting all additional values
	 * added during the sending.
	 */
	private void _reset() {
		headers.remove(HEADER_HOST);
	}

	private HttpResponse _send() {
		if (httpConnection == null) {
			open();
		}

		// sends data
		final HttpResponse httpResponse;
		try {
			OutputStream outputStream = httpConnection.getOutputStream();

			sendTo(outputStream);

			InputStream inputStream = httpConnection.getInputStream();

			httpResponse = HttpResponse.readFrom(inputStream);

			httpResponse.assignHttpRequest(this);
		} catch (IOException ioex) {
			throw new HttpException(ioex);
		}

		boolean keepAlive = httpResponse.isConnectionPersistent();

		if (!keepAlive) {
			// closes connection if keep alive is false, or if counter reached 0
			httpConnection.close();
			httpConnection = null;
		}

		return httpResponse;
	}

	// ---------------------------------------------------------------- buffer

	/**
	 * Prepares the request buffer.
	 */
	@Override
	protected Buffer buffer(final boolean fullRequest) {
		// INITIALIZATION

		// host port

		if (header(HEADER_HOST) == null) {
			setHostHeader();
		}

		// form

		Buffer formBuffer = formBuffer();

		// query string

		String queryString = queryString();

		// user-agent

		if (header("User-Agent") == null) {
			header("User-Agent", Defaults.userAgent);
		}

		// POST method requires Content-Type to be set

		if (method.equals("POST") && (contentLength() == null)) {
			contentLength(0);
		}

		// BUILD OUT

		Buffer request = new Buffer();

		request.append(method)
			.append(SPACE)
			.append(path);

		if (query != null && !query.isEmpty()) {
			request.append('?');
			request.append(queryString);
		}

		request.append(SPACE)
			.append(httpVersion)
			.append(CRLF);

		populateHeaderAndBody(request, formBuffer, fullRequest);

		return request;
	}

	// ---------------------------------------------------------------- parse

	/**
	 * Parses input stream and creates new HttpRequest object.
	 * Assumes input stream is in ISO_8859_1 encoding.
	 */
	public static HttpRequest readFrom(final InputStream in) {
		return readFrom(in, StringPool.ISO_8859_1);
	}

	public static HttpRequest readFrom(final InputStream in, final String encoding) {
		BufferedReader reader;
		try {
			reader = new BufferedReader(new InputStreamReader(in, encoding));
		} catch (UnsupportedEncodingException uneex) {
			return null;
		}

		final HttpRequest httpRequest = new HttpRequest();
		httpRequest.headers.clear();

		final String line;
		try {
			line = reader.readLine();
		} catch (IOException ioex) {
			throw new HttpException(ioex);
		}

		if (!StringUtil.isBlank(line)) {
			String[] s = StringUtil.splitc(line, ' ');

			httpRequest.method(s[0]);
			httpRequest.path(s[1]);
			httpRequest.httpVersion(s[2]);

			httpRequest.readHeaders(reader);
			httpRequest.readBody(reader);
		}

		return httpRequest;
	}

	// ---------------------------------------------------------------- shortcuts

	/**
	 * Specifies JSON content type.
	 */
	public HttpRequest contentTypeJson() {
		return contentType(MimeTypes.MIME_APPLICATION_JSON);
	}

	/**
	 * Accepts JSON content type.
	 */
	public HttpRequest acceptJson() {
		return accept(MimeTypes.MIME_APPLICATION_JSON);
	}

	// ---------------------------------------------------------------- functional/async

	/**
	 * Sends http request asynchronously using common fork-join pool.
	 * Note that this is not the right non-blocking call (not a NIO), it is just
	 * a regular call that is operated in a separate thread.
	 */
	public CompletableFuture<HttpResponse> sendAsync() {
		return CompletableFuture.supplyAsync(this::send);
	}

	/**
	 * Syntax sugar.
	 */
	public <R> R sendAndReceive(final Function<HttpResponse, R> responseHandler) {
		return responseHandler.apply(send());
	}

	/**
	 * Syntax sugar.
	 */
	public void sendAndReceive(final Consumer<HttpResponse> responseHandler) {
		responseHandler.accept(send());
	}

}


// 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.Session;
import java.util.Properties;

abstract class EMLProperties<T extends EMLProperties<T>> {

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

	/**
	 * {@link Properties}.
	 */
	private Session session;

	/**
	 * Returns the {@link Session}.
	 *
	 * @return the {@link Session}.
	 */
	protected Session getSession() {
		return session;
	}

	/**
	 * Custom {@link Properties}.
	 */
	private final Properties properties = new Properties();

	/**
	 * Returns the {@link Properties}.
	 *
	 * @return the {@link Properties}.
	 */
	protected Properties getProperties() {
		return properties;
	}

	/**
	 * Creates new {@link Session} with or without custom {@link Properties}.
	 *
	 * @param properties Custom properties to be used during {@link Session} creation. It is acceptable is this value is {@code null}.
	 * @return {@link Session} with any custom {@link Properties}
	 */
	protected Session createSession(Properties properties) {
		if (properties == null) {
			properties = System.getProperties();
		}

		this.session = Session.getInstance(properties);

		return session;
	}

	/**
	 * Copies properties from given set. If {@link Session} is already created, exception will be thrown.
	 *
	 * @param properties {@link Properties} to set.
	 * @return this
	 * @throws MailException if the {@link Properties} has already been set.
	 */
	public T set(final Properties properties) throws MailException {
		checkSessionNotSet();
		this.properties.putAll(properties);
		return _this();
	}

	/**
	 * Sets property for the {@link Session}. If {@link Session} is already created, an exception
	 * will be thrown.
	 *
	 * @param name  Property name to set.
	 * @param value Property value to set.
	 * @return this
	 * @throws MailException if the {@link Properties} has already been set.
	 */
	public T set(final String name, final String value) {
		checkSessionNotSet();
		properties.setProperty(name, value);
		return _this();
	}

	/**
	 * Uses default {@link Session}. Any property will be ignored.
	 *
	 * @return this
	 * @see System#getProperties()
	 */
	public T useDefaultSession() {
		this.session = Session.getDefaultInstance(System.getProperties());
		return _this();
	}

	/**
	 * Ensures that {@link Session} has not yet been set.
	 *
	 * @throws MailException if {@link Session} has already been set.
	 */
	private void checkSessionNotSet() throws MailException {
		if (session != null) {
			throw new MailException("Can't set properties after session is assigned");
		}
	}

}


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

package jodd.lagarto.dom;

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

/**
 * Document node is always a root node.
 * Holds various DOM-related configuration and information.
 */
public class Document extends Node {

	protected long elapsedTime;
	protected final LagartoDomBuilderConfig config;
	protected List<String> errors;

	public Document() {
		this(new LagartoDomBuilderConfig());
	}

	/**
	 * Document constructor with all relevant flags.
	 */
	public Document(final LagartoDomBuilderConfig config) {
		super(null, NodeType.DOCUMENT, null);
		this.config = config;
		this.elapsedTime = System.currentTimeMillis();
	}

	@Override
	public Document clone() {
		Document document = cloneTo(new Document(config));
		document.elapsedTime = this.elapsedTime;
		return document;
	}

	/**
	 * Notifies document that parsing is done.
	 */
	protected void end() {
		elapsedTime = System.currentTimeMillis() - elapsedTime;
	}

	@Override
	protected void visitNode(final NodeVisitor nodeVisitor) {
		nodeVisitor.document(this);
	}

	// ---------------------------------------------------------------- errors

	/**
	 * Add new error message to the {@link #getErrors() errors list}.
	 * If errors are not collected error, message is ignored.
	 */
	public void addError(final String message) {
		if (config.collectErrors) {
			if (errors == null) {
				errors = new ArrayList<>();
			}
			errors.add(message);
		}
	}

	/**
	 * Returns list of warnings and errors occurred during parsing.
	 * Returns null if parsing was successful; or if
	 * errors are not collected.
	 */
	public List<String> getErrors() {
		return errors;
	}

	// ---------------------------------------------------------------- attr

	/**
	 * Document node does not have attributes.
	 */
	@Override
	public void setAttribute(final String name, final String value) {
	}

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

	/**
	 * Returns DOM building elapsed time.
	 */
	public long getElapsedTime() {
		return elapsedTime;
	}

	/**
	 * Returns used {@link jodd.lagarto.dom.LagartoDomBuilderConfig}.
	 */
	public LagartoDomBuilderConfig getConfig() {
		return config;
	}

}