/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.reflect;

import org.slf4j.Logger;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Callable;

/**
 * Utils methods for reflection.
 *
 * @author ypujante@linkedin.com
 *
 */
public class ReflectUtils
{
  public static final String MODULE = ReflectUtils.class.getName();
  public static final Logger log = org.slf4j.LoggerFactory.getLogger(MODULE);

  private final static Method CLONE_METHOD;

  static
  {
    try
    {
      CLONE_METHOD = Object.class.getDeclaredMethod("clone");
    }
    catch(NoSuchMethodException e)
    {
      throw new RuntimeException(e);
    }
  }

  /**
   * Given a proxy returns the object proxied. If the object is not a proxy, then return the object
   * itself.
   *
   * @param proxy
   * @return the proxied object (or proxy if not an object proxy)
   */
  @SuppressWarnings("unchecked")
  public static Object getProxiedObject(Object proxy)
  {
    if(Proxy.isProxyClass(proxy.getClass()))
    {
      InvocationHandler invocationHandler = Proxy.getInvocationHandler(proxy);
      if(invocationHandler instanceof ObjectProxy)
      {
        ObjectProxy objectProxy = (ObjectProxy) invocationHandler;
        // recursively fetch the proxy
        return getProxiedObject(objectProxy.getProxiedObject());
      }
      else
        return proxy;
    }
    else
      return proxy;

  }

  /**
   * Clones the object. If the object is not cloneable then throws an exception. Basically
   * this method calls clone on the object by using reflection.
   *
   * @param object the object to clone
   * @return the cloned object
   * @throws CloneNotSupportedException if the object is not cloneable
   */
  @SuppressWarnings("unchecked")
  public static <T> T clone(T object) throws CloneNotSupportedException
  {
    if(!(object instanceof Cloneable))
    {
      throw new CloneNotSupportedException();
    }

    try
    {
      return (T) CLONE_METHOD.invoke(object);
    }
    catch(IllegalAccessException e)
    {
      CloneNotSupportedException ex = new CloneNotSupportedException();
      ex.initCause(e);
      throw ex;
    }
    catch(InvocationTargetException e)
    {
      try
      {
        throw e.getCause();
      }
      catch(CloneNotSupportedException ex)
      {
        throw ex;
      }
      catch(RuntimeException ex)
      {
        throw ex;
      }
      catch(Throwable throwable)
      {
        throw new RuntimeException(throwable);
      }
    }
  }

  /**
   * Return a the default class loader (the one associated to the current thread or the one from
   * this class)
   *
   * @return the default ClassLoader (never null)
   * @see java.lang.Thread#getContextClassLoader()
   */
  public static ClassLoader getDefaultClassLoader()
  {
    ClassLoader cl = null;
    try
    {
      cl = Thread.currentThread().getContextClassLoader();
    }
    catch(Throwable ex)
    {
      if(log.isDebugEnabled())
        log.debug("Cannot access thread context ClassLoader - falling back to system class loader",
                   ex);
    }
    if(cl == null)
    {
      // No thread context class loader -> use class loader of this class.
      cl = ReflectUtils.class.getClassLoader();
    }
    return cl;
  }

  /**
   * Wraps Class.forName to be context class loader aware.
   *
   * @param name the name of the Class
   * @return Class instance for the supplied name
   * @throws ClassNotFoundException if the class was not found
   * @throws LinkageError           if the class file could not be loaded
   * @see Class#forName(String, boolean, ClassLoader)
   * @see #getDefaultClassLoader()
   */
  public static Class forName(String name) throws ClassNotFoundException, LinkageError
  {
    return forName(name, getDefaultClassLoader());
  }

  /**
   * Wraps Class.forName to be context class loader aware.
   *
   * @param name        the name of the Class
   * @param classLoader the class loader to use (may be null, which indicates the
   *                    default class loader)
   * @return Class instance for the supplied name
   * @throws ClassNotFoundException if the class was not found
   * @throws LinkageError           if the class file could not be loaded
   * @see Class#forName(String, boolean, ClassLoader)
   */
  public static Class forName(String name, ClassLoader classLoader)
    throws ClassNotFoundException, LinkageError
  {
    if(classLoader == null)
    {
      classLoader = getDefaultClassLoader();
    }

    return Class.forName(name, true, classLoader);
  }

  /**
   * Convenient call to return a class in a different class loader... note that it will handle
   * correctly a class in the same class loader
   *
   * @param clazz       the clazz you want in a different class loader
   * @param classLoader the class loader to use (may be null, which indicates the
   *                    default class loader)
   * @return Class instance for the supplied name
   * @throws ClassNotFoundException if the class was not found
   * @throws LinkageError           if the class file could not be loaded
   * @see Class#forName(String, boolean, ClassLoader)
   */
  public static Class forName(Class clazz, ClassLoader classLoader)
    throws ClassNotFoundException, LinkageError
  {
    if(clazz == null)
      return null;

    if(classLoader == null)
    {
      classLoader = getDefaultClassLoader();
    }

    if(clazz.getClassLoader() == null || clazz.getClassLoader().equals(classLoader))
      return clazz;
    else
      return forName(clazz.getName(), classLoader);
  }

  /**
   * Execute the call within the given class loader... handle setting / reverting to
   * previous class loader in a safe manner.
   *
   * @param classLoader the class loader to set for the duration of the call
   * @param callable the callable to execute within the context of the provided class loader
   * @return the result of the call
   * @throws Exception whatever callable throws
   */
  public static <T> T executeWithClassLoader(ClassLoader classLoader,
                                             Callable<T> callable) throws Exception
  {
    ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
    try
    {
      Thread.currentThread().setContextClassLoader(classLoader);
      return callable.call();
    }
    finally
    {
      Thread.currentThread().setContextClassLoader(previousClassLoader);
    }
  }

  /**
   * Computes the signature of a method. Note that if 2 methods are declared
   * in different classes or interfaces then I1.m and I2.m are different but
   * they can still have the same signature...
   *
   * @param m method
   * @return the signature (unique per method definition)
   */
  @SuppressWarnings("unchecked")
  public static String computeSignature(Method m)
  {
    StringBuilder sb = new StringBuilder();

    sb.append(m.getName()).append('(');
    Class[] parameterTypes = m.getParameterTypes();

    for (int i = 0; i < parameterTypes.length; i++)
    {
      if(i > 0)
        sb.append(',');
      Class parameterType = parameterTypes[i];
      sb.append(parameterType.getName());
    }
    sb.append(')');

    return sb.toString();
  }

  /**
   * Utility which extract the interfaces implemented by o: it goes up the chain of inheritance
   * to find all the interfaces.
   *
   * @param o
   * @return all the interfaces implemented by o
   */
  public static Class[] extractAllInterfaces(Object o)
  {
    Set<Class> interfaces = new LinkedHashSet<Class>();

    Class c = o.getClass();
    do
    {
      interfaces.addAll(Arrays.asList(c.getInterfaces()));
      c = c.getSuperclass();
    }
    while(c != null);

    return interfaces.toArray(new Class[interfaces.size()]);
  }

  /**
   * The purpose of this method is somewhat to provide a better naming / documentation than the javadoc
   * of Class.isAssignableFrom method. Every time I need to use this method I read
   * and reread the javadoc 25 times without being clear of what should be the order of each parameter.
   *

   * What you usually want to do is:
   *

   * if o1 is an object you write something like
   *   if(o1 instanceof C)
   * But if you have only the class of o1 (say C1), then you write
   *   if(isSubClassOrInterfaceOf(C1, C))
   *
   * in other words:
   * o1 instanceof C == isSubClassOrInterfaceOf(o1.getClass(), C)
   *

   * @param subclass
   * @param superclass
   * @return true if subclass is a subclass or sub interface of superclass
   */
  public static boolean isSubClassOrInterfaceOf(Class subclass, Class superclass)
  {
    return superclass.isAssignableFrom(subclass);
  }

  /**
   * Constructor
   */
  private ReflectUtils()
  {
  }

}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.reflect;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author ypujante@linkedin.com
 *
 */
public class ObjectProxyBuilder
{
  /**
   * Convenient call which creates a proxy using the handler and the interface. It will also
   * check that the object bien proxied (if implements ObjectProxy) properly
   * implement the interface...
   *
   * @return the proxy (which implement all the interface)
   */
  @SuppressWarnings("unchecked")
  public static <T> T createProxy(InvocationHandler handler, Class<T> itface)
  {
    if(handler instanceof ObjectProxy)
    {
      ObjectProxy proxy = (ObjectProxy) handler;
      if(!ReflectUtils.isSubClassOrInterfaceOf(proxy.getProxiedObject().getClass(), itface))
        throw new IllegalArgumentException(proxy.getProxiedObject().getClass() + " does not extend " + itface);
    }

    return (T) Proxy.newProxyInstance(itface.getClassLoader(),
                                      new Class<?>[] {itface},
                                      handler);
  }

  /**
   * Convenient call which creates a proxy using the handler and the interfaces. It will also
   * check that the object bien proxied (if implements ObjectProxy) properly
   * implement the right interfaces...
   *
   * @return the proxy (which implement all the provided interfaces)
   */
  public static Object createProxy(InvocationHandler handler, Class<?>... itfaces)
  {
    if(handler instanceof ObjectProxy)
    {
      ObjectProxy proxy = (ObjectProxy) handler;
      Class<?> proxyClass = proxy.getProxiedObject().getClass();
      for(Class<?> itface : itfaces)
      {
        if(!ReflectUtils.isSubClassOrInterfaceOf(proxyClass, itface))
          throw new IllegalArgumentException(proxyClass + " does not extend " + itface);
      }
    }

    List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
    for(Class<?> itface : itfaces)
    {
      classLoaders.add(itface.getClassLoader());
    }

    ClassLoader classLoader = ClassLoaderChain.createChain(classLoaders);

    return Proxy.newProxyInstance(classLoader,
                                  itfaces,
                                  handler);
  }

  /**
   * @see #createProxy(InvocationHandler, Class[])
   */
  public static Object createProxy(InvocationHandler handler, Collection<Class<?>> interfaces)
  {
    return createProxy(handler, interfaces.toArray(new Class[interfaces.size()]));
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.concurrent;

import org.linkedin.util.clock.Clock;
import org.linkedin.util.clock.ClockUtils;
import org.linkedin.util.clock.SystemClock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Condition;

/**
 * @author ypujante@linkedin.com
 *
 */
public class ConcurrentUtils
{
  /**
   * Waits on the condition but if end time is expired then throws an
   * exception. On purpose this method does not synchronize on the lock
   * because it must be called from a block which synchronizes on it
   *
   * @param condition the condition to wait on
   * @param endTime the time after which an exception is thrown (compared
   * to {@link java.lang.System#currentTimeMillis()}. If < 0 then wait
   * indefinitely (so TimeOutException will not be thrown!)
   * @throws InterruptedException if the thread is interrupted while waiting
   * @throws TimeoutException if the time has been reached */
  public static void awaitUntil(Condition condition, long endTime)
    throws InterruptedException, TimeoutException
  {
    awaitUntil(SystemClock.INSTANCE, condition, endTime);
  }

  /**
   * Waits on the condition but if end time is expired then throws an
   * exception. On purpose this method does not synchronize on the lock
   * because it must be called from a block which synchronizes on it
   *
   * @param condition the condition to wait on
   * @param endTime the time after which an exception is thrown (compared
   * to {@link java.lang.System#currentTimeMillis()}. If < 0 then wait
   * indefinitely (so TimeOutException will not be thrown!)
   * @throws InterruptedException if the thread is interrupted while waiting
   * @throws TimeoutException if the time has been reached */
  public static void awaitUntil(Clock clock, Condition condition, long endTime)
    throws InterruptedException, TimeoutException
  {
    if(endTime <= 0)
      condition.await();
    else
    {
      long now = clock.currentTimeMillis();
      if(now >= endTime)
        throw new TimeoutException("timeout reached while waiting on the lock: "
                                   + condition);

      if(!condition.await(endTime - now, TimeUnit.MILLISECONDS))
        throw new TimeoutException("timeout reached while waiting on the lock: "
                                   + condition);
    }
  }

  /**
   * Waits on the lock but if end time is expired then throws an
   * exception. On purpose this method does not synchronize on the lock
   * because it must be called from a block which synchronizes on it
   *
   * @param lock the lock to wait on
   * @param endTime the time after which an exception is thrown (compared
   * to {@link java.lang.System#currentTimeMillis()}. If < 0 then wait
   * indefinitely (so TimeOutException will not be thrown!)
   * @throws InterruptedException if the thread is interrupted while waiting
   * @throws TimeoutException if the time has been reached */
  public static void awaitUntil(Object lock, long endTime)
    throws InterruptedException, TimeoutException
  {
    awaitUntil(SystemClock.INSTANCE, lock, endTime);
  }

  /**
   * Waits on the lock but if end time is expired then throws an
   * exception. On purpose this method does not synchronize on the lock
   * because it must be called from a block which synchronizes on it
   *
   * @param lock the lock to wait on
   * @param endTime the time after which an exception is thrown (compared
   * to {@link java.lang.System#currentTimeMillis()}. If < 0 then wait
   * indefinitely (so TimeOutException will not be thrown!)
   * @throws InterruptedException if the thread is interrupted while waiting
   * @throws TimeoutException if the time has been reached */
  public static void awaitUntil(Clock clock, Object lock, long endTime)
    throws InterruptedException, TimeoutException
  {
    if(endTime <= 0)
      lock.wait();
    else
    {
      long now = clock.currentTimeMillis();
      if(now >= endTime)
        throw new TimeoutException("timeout reached while waiting on the lock: "
                                   + lock);
      lock.wait(endTime - now);
    }
  }

  /**
   * Joins on the thread but if end time is expired then throws an exception.
   *
   * @param thread the thread to join on
   * @param endTime the time after which an exception is thrown (compared
   * to {@link java.lang.System#currentTimeMillis()}. If <= 0 then wait
   * indefinitely (so TimeOutException will not be thrown!)
   * @throws InterruptedException if the thread is interrupted while waiting
   * @throws TimeoutException if the time has been reached
   */
  public static void joinUntil(Clock clock, Thread thread, long endTime)
    throws InterruptedException, TimeoutException
  {
    if(endTime <= 0)
      thread.join();
    else
    {
      while(thread.isAlive())
      {
        long now = clock.currentTimeMillis();
        if(now >= endTime)
          throw new TimeoutException("timeout reached while joining on: " + thread);
        thread.join(endTime - now);
      }
    }
  }

  /**
   * Joins on the thread but if end time is expired then throws an exception.
   *
   * @param thread the thread to join on
   * @param timeout how long to join for (null or <= 0 means forever)
   * @throws InterruptedException if the thread is interrupted while waiting
   * @throws TimeoutException if the time has been reached
   */
  public static void joinFor(Clock clock, Thread thread, Object timeout)
    throws InterruptedException, TimeoutException
  {
    joinUntil(clock, thread, ClockUtils.toEndTime(timeout));
  }

  /**
   * Constructor
   */
  private ConcurrentUtils()
  {
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.reflect;

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

/**
 * Very basic implementation of the proxy which simply delegates all calls to the proxied object.
 *
 * @author ypujante@linkedin.com
 *
 */
public class ObjectProxyInvocationHandler<T> implements ObjectProxy<T>, InvocationHandler
{
  private final T _proxiedObject;

  /**
   * Constructor
   */
  public ObjectProxyInvocationHandler(T proxiedObject)
  {
    _proxiedObject = proxiedObject;
  }

  /**
   * @return the object proxied
   */
  @Override
  public T getProxiedObject()
  {
    return _proxiedObject;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  {
    try
    {
      return method.invoke(_proxiedObject, args);
    }
    catch(InvocationTargetException e)
    {
      throw e.getTargetException();
    }
  }

  /**
   * This is a convenient call when you want to proxy an object with the unique given
   * interface. Note that you can use the more general call {@link Proxy#newProxyInstance(ClassLoader, Class[], InvocationHandler)}
   * if you want to do something different.
   *
   * @param interfaceToProxy the interface to proxy
   * @param objectToProxy the object to proxy (note that it must implement interfaceToProxy!)
   * @return the proxied object  */
  @SuppressWarnings("unchecked")
  public static <T> T createProxy(Class<T> interfaceToProxy, T objectToProxy)
  {
    // since there is only one interface in the array, we know that the proxy must implement it!
    return (T) Proxy.newProxyInstance(interfaceToProxy.getClassLoader(),
                                      new Class<?>[] {interfaceToProxy},
                                      new ObjectProxyInvocationHandler(objectToProxy));
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.io.resource;

import org.linkedin.util.io.resource.internal.AbstractResource;
import org.linkedin.util.io.resource.internal.InternalResourceProvider;
import org.linkedin.util.io.resource.internal.InternalResource;
import org.linkedin.util.io.resource.internal.ResourceProviderChain;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;

/**
 * @author ypujante@linkedin.com
 *
 */
public class ResourceChain extends AbstractResource
{
  private final Resource _resource;

  /**
   * Constructor
   */
  public ResourceChain(InternalResourceProvider resourceProvider, String path, Resource resource)
  {
    super(resourceProvider, path);
    _resource = resource;
  }

  /**
   * @return true if the resource exists.
   */
  @Override
  public boolean exists()
  {
    return _resource.exists();
  }

  /**
   * Returns a File handle for this resource.
   *
   * @throws IOException if the resource cannot be resolved as a File handle, i.e. it
   *                     is not available on the file system (or it cannot be made available).
   */
  @Override
  public File getFile() throws IOException
  {
    return _resource.getFile();
  }

  /**
   * Important note: the caller of this method is responsible for properly closing the input
   * stream!
   *
   * @return an input stream to the resource.
   * @throws IOException if cannot get an input stream
   */
  @Override
  public InputStream getInputStream() throws IOException
  {
    return _resource.getInputStream();
  }

  /**
   * Efficiently returns all information about the resource.
   *
   * @return information about this resource.
   * @throws IOException if cannot get information
   */
  @Override
  public ResourceInfo getInfo() throws IOException
  {
    return _resource.getInfo();
  }

  /**
   * @return true if this resource represents a directory.
   */
  @Override
  public boolean isDirectory()
  {
    return _resource.isDirectory();
  }

  /**
   * @return a uri representation of the resource
   */
  @Override
  public URI toURI()
  {
    return _resource.toURI();
  }

  /**
   * Returns true if this resource was modified since the time provided. A trivial
   * implementation is return lastModified() > time, but various implementations can
   * provide better alternatives. If the resource does not exsit then it returns
   * false.
   *
   * @param time the time to check against
   * @return a boolean
   */
  @Override
  public boolean isModifiedSince(long time)
  {
    return _resource.isModifiedSince(time);
  }

  /**
   * Convenient method to create a chain from a list of resources.
   *
   * @param resources the list of resources.
   * @return the resource
   */
  public static Resource create(Resource... resources)
  {
    return create(Arrays.asList(resources));
  }

  /**
   * Convenient method to create a chain from a list of resources. All the resources are turned into
   * roots before chaining (otherwise it does not make a lot of sense...)
   *
   * @param resources the list of resources.
   * @return the resource
   */
  public static Resource create(List<Resource> resources)
  {
    if(resources.size() == 1)
      return resources.get(0).chroot(".");

    List<InternalResourceProvider> providers =
      new ArrayList<InternalResourceProvider>(resources.size());

    for(Resource resource : resources)
    {
      resource = resource.chroot(".");

      InternalResourceProvider provider =
        (InternalResourceProvider) ((InternalResource) resource).getResourceProvider();

      if(provider instanceof ResourceProviderChain)
      {
        ResourceProviderChain resourceProviderChain = (ResourceProviderChain) provider;
        providers.addAll(resourceProviderChain.getResourceProviders());
      }
      else
      {
        providers.add(provider);
      }
    }

    return new ResourceProviderChain(providers).getRootResource();
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.codec;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * This codec will computes the digest of the input and then encode the result
 * with the codec. It is one way only because there is no way to go back to
 * the input. This class is thread safe!
 *
 * @author ypujante@linkedin.com
 */
public class OneWayMessageDigestCodec implements OneWayCodec
{
  public static final String MODULE = OneWayMessageDigestCodec.class.getName();

  private final MessageDigest _md;
  private final OneWayCodec _codec;

  /**
   * Constructor.
   *
   * @param algorithm the algorithm to use for the message digest
   * @param codec the codec to do the byte to String
   * encoding
   * @throws NoSuchAlgorithmException if the algorithm is invalid
   * @throws CloneNotSupportedException if the message digest is not cloneable */
  public OneWayMessageDigestCodec(String algorithm, OneWayCodec codec)
    throws NoSuchAlgorithmException, CloneNotSupportedException
  {
    this(algorithm, null, codec);
  }

  /**
   * Constructor
   *
   * @param algorithm the algorithm to use for the message digest
   * @param password the password to initialize the message digest
   * @param codec the codec to do the byte to String
   * encoding
   * @throws NoSuchAlgorithmException if the algorithm is invalid
   * @throws CloneNotSupportedException if the message digest is not cloneable */
  public OneWayMessageDigestCodec(String algorithm,
                                  String password,
                                  OneWayCodec codec)

    throws NoSuchAlgorithmException, CloneNotSupportedException
  {
    _md = MessageDigest.getInstance(algorithm);
    if(password != null)
      _md.update(password.getBytes());
    _md.clone(); // verify that it is cloneable
    _codec = codec;
  }

  /**
   * Constructor */
  public OneWayMessageDigestCodec(MessageDigest md, OneWayCodec codec)
    throws CloneNotSupportedException
  {
    _md = md;
    _md.clone(); // verify that it is cloneable
    _codec = codec;
  }

  /**
   * Encode the array into a String
   *
   * @param byteArray the array to encode
   * @return the encoded String */
  @Override
  public String encode(byte[] byteArray)
  {
    MessageDigest md = null;

    try
    {
      md = (MessageDigest) _md.clone();
    }
    catch(CloneNotSupportedException e)
    {
      // should not happen... already tested in constructor!!
      throw new RuntimeException(e);
    }

    return _codec.encode(md.digest(byteArray));
  }

  /**
   * Convenient call which create an instance based on the MD5 algorithm.
   *
   * @param codec the codec to do the byte to String
   * encoding
   * @return the codec */
  public static OneWayMessageDigestCodec createMD5Instance(OneWayCodec codec)
  {
    return createMD5Instance(null, codec);
  }

  /**
   * Convenient call which create an instance based on the MD5 algorithm.
   *
   * @param password the password to use for MD5
   * @param codec the codec to do the byte to String
   * encoding
   * @return the codec */
  public static OneWayMessageDigestCodec createMD5Instance(String password,
                                                           OneWayCodec codec)
  {
    return createWellKnownInstance("MD5", password, codec);
  }

  /**
   * Convenient call which create an instance based on the SHA-1 algorithm.
   *
   * @param codec the codec to do the byte to String
   * encoding
   * @return the codec */
  public static OneWayMessageDigestCodec createSHA1Instance(OneWayCodec codec)
  {
    return createSHA1Instance(null, codec);
  }

  /**
   * Convenient call which create an instance based on the SHA-1 algorithm.
   *
   * @param password the password to use for SHA-1
   * @param codec the codec to do the byte to String
   * encoding
   * @return the codec */
  public static OneWayMessageDigestCodec createSHA1Instance(String password,
                                                            OneWayCodec codec)
  {
    return createWellKnownInstance("SHA-1", password, codec);
  }

  /**
   * This creates a well known instance.. the difference with the constructor
   * is that all exceptions are caught and rethrown as RuntimeExcpetion
   * because they should not happen.
   *
   * @param algorithm
   * @param password
   * @param codec
   * @return the codec  */
  private static OneWayMessageDigestCodec createWellKnownInstance(String algorithm,
                                                                  String password,
                                                                  OneWayCodec codec)
  {
    try
    {
      return new OneWayMessageDigestCodec(algorithm, password, codec);
    }
    catch(NoSuchAlgorithmException e)
    {
      throw new RuntimeException(e);
    }
    catch(CloneNotSupportedException e)
    {
      throw new RuntimeException(e);
    }
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.io.resource.internal;

import org.linkedin.util.io.PathUtils;
import org.linkedin.util.io.ram.RAMDirectory;
import org.linkedin.util.io.ram.RAMEntry;
import org.linkedin.util.io.resource.RAMResource;
import org.linkedin.util.io.resource.ResourceFilter;

import java.io.IOException;

/**
 * @author ypujante@linkedin.com
 *
 */
public class RAMResourceProvider extends PathBasedResourceProvider
{
  private final RAMDirectory _root;

  /**
   * Constructor
   */
  public RAMResourceProvider(RAMDirectory root)
  {
    this(root, "/");
  }

  /**
   * Constructor
   */
  public RAMResourceProvider(RAMDirectory root, String rootPath)
  {
    super(rootPath);
    _root = root;
  }

  /**
   * Builds a resource given a path. Only subclasses know how to do that.
   *
   * @param path the path to the new resource (always starts with /)
   * @return the resource
   */
  @Override
  public InternalResource doBuildResource(String path)
  {
    return new RAMResource(this, getFullPath(path), path);
  }

  /**
   * Creates a new resource provider given the new path.
   *
   * @param rootPath
   * @return the new resource provider
   */
  @Override
  public InternalResourceProvider doCreateResourceProvider(String rootPath)
  {
    return new RAMResourceProvider(_root, getFullPath(rootPath));
  }

  /**
   * If the path denotes a directory, then it will return all resources that are contained in the
   * directory.
   *
   * @param path   the path to the resource
   * @param filter the filter to include only some resources in the result
   * @return true if it was a directory, false otherwise
   * @throws IOException if there is an error accessing the resource
   */
  @Override
  public boolean doList(String path, ResourceFilter filter)
  {
    if(_root == null)
      return false;

    RAMEntry entry = _root.getEntryByPath(path);

    if(entry instanceof RAMDirectory)
    {
      RAMDirectory ramDirectory = (RAMDirectory) entry;
      for(RAMEntry ramEntry : ramDirectory.ls())
      {
        String resourcePath = PathUtils.addPaths(path, ramEntry.name());
        filter.accept(new RAMResource(this,
                                      getFullPath(resourcePath),
                                      resourcePath));
      }
      return true;
    }
    else
      return false;
  }

  public RAMEntry getRAMEntry(String fullpath)
  {
    return _root.getEntryByPath(fullpath);
  }

  @Override
  public boolean equals(Object o)
  {
    if(this == o) return true;
    if(o == null || getClass() != o.getClass()) return false;
    if(!super.equals(o)) return false;

    RAMResourceProvider that = (RAMResourceProvider) o;

    if(_root != null ? !_root.equals(that._root) : that._root != null) return false;

    return true;
  }

  @Override
  public int hashCode()
  {
    int result = super.hashCode();
    result = 31 * result + (_root != null ? _root.hashCode() : 0);
    return result;
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.url;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.StringTokenizer;

import org.linkedin.util.text.StringSplitter;

/**
 * Convenient class to build a URL. (not thread safe!).
 *
 * @author ypujante@linkedin.com
 */
public class URLBuilder implements URL, Serializable
{
  private static final long serialVersionUID = 1L;

  private static StringSplitter SS = new StringSplitter('/');

  private String _scheme = null;
  private String _userInfo = null;
  private String _host = null;
  private int _port = -1;
  private String _path = null;
  private QueryBuilder _query = null;
  private String _fragment = null;
  private boolean _escapeFragment = true;

  /**
   * Constructor */
  public URLBuilder()
  {
    _query = new QueryBuilder();
  }

  /**
   * Copy Constructor
   * @param other
   */
  private URLBuilder(URLBuilder other)
  {
    _scheme = other._scheme;
    _userInfo = other._userInfo;
    _host = other._host;
    _port = other._port;
    _path = other._path;
    _query = other._query.deepClone();
    _fragment = other._fragment;
  }

  @Override
  public String getScheme()
  {
    return _scheme;
  }

  public void setScheme(String scheme)
  {
    _scheme = scheme;
  }

  @Override
  public boolean getHasScheme()
  {
    return _scheme != null;
  }

  @Override
  public String getUserInfo()
  {
    return _userInfo;
  }

  public void setUserInfo(String userInfo)
  {
    _userInfo = userInfo;
  }

  @Override
  public boolean getHasUserInfo()
  {
    return _userInfo != null;
  }

  @Override
  public String getHost()
  {
    return _host;
  }

  public void setHost(String host)
  {
    _host = host;
  }

  @Override
  public boolean getHasHost()
  {
    return _host != null;
  }

  @Override
  public int getPort()
  {
    return _port;
  }

  public void setPort(int port)
  {
    _port = port;
  }

  @Override
  public boolean getHasPort()
  {
    return _port > -1;
  }

  @Override
  public String getPath()
  {
    return _path;
  }

  /**
   * No check or escaping is done. Use this call to set a relative path.
   *
   * @param path the value to set */
  public void setPath(String path)
  {
    if("".equals(path))
      path = null;

    _path = path;
  }

  /**
   * Appends the given path. The path is escaped: all / are turned into %2d.
   * After this call, newpath = oldpath + / + path.
   * As a consequence, this method cannot be used to set a relative path.
   *
   * @param path the path to append */
  public URLBuilder appendPath(String path)
  {
    appendPath(path, true);
    return this;
  }

  /**
   * Appends the given path. The path is conditionaly escaped.
   * After this call, newpath = oldpath + / + path.
   * As a consequence, this method cannot be used to set a relative path.
   *
   * @param path the path to append
   * @param encodePath true to encode the path */
  public void appendPath(String path, boolean encodePath)
  {
    if(path == null)
      return;

    if(encodePath)
      path = urlEncode(path);

    if(path.startsWith("/"))
      path = path.substring(1);

    if(_path == null || "".equals(_path))
    {
      StringBuilder sb = new StringBuilder("/");
      sb.append(path);
      _path = sb.toString();
    }
    else
    {
      StringBuilder sb = new StringBuilder(_path);

      if(_path.endsWith("/"))
        sb.append(path);
      else
        sb.append('/').append(path);

      _path = sb.toString();
    }
  }

  /**
   * @return true if a path has been set */
  @Override
  public boolean getHasPath()
  {
    return _path != null && _path.length() > 0;
  }

  /**
   * @return the path splitted at each '/' and decoded
   */
  @Override
  public List<String> getPathComponents()
  {
    if(getHasPath())
    {
      String path = getPath();

      if("/".equals(path))
        return Collections.emptyList();

      // if path starts with "/"
      if(path.startsWith("/"))
        path = path.substring(1);

      if(path.endsWith("/"))
        path = path.substring(0, path.length() - 1);

      List<String> pathComponents = SS.splitAsList(path);

      ListIterator<String> iter = pathComponents.listIterator();
      while(iter.hasNext())
      {
        String pathComponent = iter.next();
        iter.set(urlDecode(pathComponent));
      }

      return pathComponents;
    }
    else
    {
      return Collections.emptyList();
    }
  }

  @Override
  public String getFragment()
  {
    return _fragment;
  }

  /**
   * @param fragment the fragment to set (after #)
   */
  public void setFragment(String fragment)
  {
    _fragment = fragment;
  }

  /**
   * Changes whether we'll uri escape the fragment.  If this is supplied the fragment must be
   * in *uric format (see RFC2396)
   *
   * Note that this class incorrectly encodes the fragment.
   *
   * @param escapeFragment whether or not to escape the fragment
   */
  public URLBuilder setEscapeFragment(boolean escapeFragment) {
    _escapeFragment = escapeFragment;
    return this;
  }

  /**
   * @return true if the fragment has been set */
  @Override
  public boolean getHasFragment()
  {
    return _fragment != null;
  }

  @Override
  public String getQueryString()
  {
    return _query.getQuery();
  }

  @Override
  public Query getQuery()
  {
    return _query;
  }

//  public QueryBuilder getQueryBuilder()
//  {
//    return _query;
//  }

  /**
   * @return true if query parameters have been added */
  @Override
  public boolean getHasQueryParameters()
  {
    return _query.getHasQueryParameters();
  }

  /**
   * @return the url */
  @Override
  public String getURL()
  {
    String query = _query.getQuery();
    // this code has been copied from URI class
    StringBuilder sb = new StringBuilder();
    if(_scheme != null)
    {
      sb.append(_scheme);
      sb.append(':');
    }

    if(_host != null)
    {
      sb.append("//");
      if(_userInfo != null)
      {
        sb.append(_userInfo);
        sb.append('@');
      }

      boolean needBrackets = ((_host.indexOf(':') >= 0)
                              && !_host.startsWith("[")
                              && !_host.endsWith("]"));
      if(needBrackets) sb.append('[');
      sb.append(_host);
      if(needBrackets) sb.append(']');
      if(_port != -1)
      {
        sb.append(':');
        sb.append(_port);
      }
    }
    if (_path != null)
      sb.append(_path);
    if(!"".equals(query))
      sb.append('?').append(query);
    if(_fragment != null) {
      sb.append('#').append(_escapeFragment ? urlEncode(_fragment) : _fragment);
    }
    return sb.toString();
  }

  /**
   * @return this object as a {@link URL}
   */
  @Override
  public java.net.URL toJavaURL()
  {
    try
    {
      return new java.net.URL(getURL());
    }
    catch(MalformedURLException e)
    {
      // shouldn't happen because this class always builds well formed URLs...
      throw new RuntimeException(e);
    }
  }

  /**
   * Adds the query parameter (no value)
   *
   * @param name
   * @return this object for chaining */
  public URLBuilder addQueryParameter(String name)
  {
    _query.addParameter(name);
    return this;
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value
   * @return this object for chaining */
  public URLBuilder addQueryParameter(String name, String value)
  {
    if(value == null)
      throw new IllegalArgumentException("value is null for " + name);

    _query.addParameter(name, value);
    return this;
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value
   * @return this object for chaining */
  public URLBuilder addQueryParameter(String name, int value)
  {
    _query.addParameter(name, value);
    return this;
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value
   * @return this object for chaining */
  public URLBuilder addQueryParameter(String name, boolean value)
  {
    _query.addBooleanParameter(name, value);
    return this;
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param values
   * @return this object for chaining */
  public URLBuilder addQueryParameter(String name, int [] values)
  {
    String [] params = new String[values.length];
    int idx = 0;

    if (values.length > 0)
    {
      for (int value: values)
      {
        params[idx++] = String.valueOf(value);
      }

      _query.addParameters(name, params);
    }

    return this;
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param values
   * @return this object for chaining
   */
  public URLBuilder addQueryParameter(String name, String[] values)
  {
    if (values != null && values.length > 0)
    {
      _query.addParameters(name, values);
    }
    return this;
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value
   * @return this object for chaining */
  public URLBuilder addQueryParameter(String name, long value)
  {
    _query.addParameter(name, value);
    return this;
  }

  /**
   * @see QueryBuilder#addIndexedParameter(String, int, int)
   * @return this object for chaining */
  public URLBuilder addQueryIndexedParameter(String name, int value, int index)
  {
    _query.addIndexedParameter(name, value, index);
    return this;
  }

  /**
   * @see QueryBuilder#addIndexedParameter(String, int, int...)
   * @return this object for chaining */
  public URLBuilder addQueryIndexedParameter(String name, int value, int... indices)
  {
    _query.addIndexedParameter(name, value, indices);
    return this;
  }

  /**
   * @see QueryBuilder#addIndexedParameter(String, String, int)
   * @return this object for chaining */
  public URLBuilder addQueryIndexedParameter(String name, String value, int index)
  {
    _query.addIndexedParameter(name, value, index);
    return this;
  }

  /**
   * @see QueryBuilder#addIndexedParameter(String, String, int...)
   * @return this object for chaining */
  public URLBuilder addQueryIndexedParameter(String name, String value, int... indices)
  {
    _query.addIndexedParameter(name, value, indices);
    return this;
  }

  /**
   * Adds the query parameter. If value is true then
   * adds the name as the value, else don't add it at all.
   *
   * @param name
   * @param value
   * @return this object for chaining */
  public URLBuilder addQueryBooleanParameter(String name, boolean value)
  {
    _query.addBooleanParameter(name, value);
    return this;
  }

  /**
   * Adds the query parameters
   *
   * @param parameters
   * @return this object for chaining
   */
  public URLBuilder addQueryParameters(Map<String, String[]> parameters)
  {
    _query.addParameters(parameters);
    return this;
  }

  /**
   * Simply adds the query provided. It is assumed that the query is properly
   * encoded for url!
   *
   * @param query the properly url encoded query
   * @return this object for chaining  */
  public URLBuilder addQuery(String query) throws URISyntaxException
  {
    _query.addQuery(query);
    return this;
  }

  /**
   * Simply adds the query provided.
   *
   * @param query the query */
  public URLBuilder addQuery(Query query)
  {
    _query.addQuery(query);
    return this;
  }

  /**
   * Simply adds the query provided.
   *
   * @param uri the query */
  public URLBuilder addQuery(URI uri)
  {
    _query.addQuery(uri);
    return this;
  }

  /**
   * Get the first query parameter given its name
   *
   * @param name the name of the parameter
   * @return the first parameter
   */
  public String getQueryParameter(String name)
  {
    return _query.getParameter(name);
  }

  /**
   * Gets the parameters given its name
   *
   * @param name the name of the parameter
   * @return the parameters or null if none found
   */
  public String[] getQueryParameterValues(String name)
  {
    return _query.getParameterValues(name);
  }

  /**
   * Remove parameter with given name and
   * return its previous value.
   *
   * @param name parameter to remove
   * @return previous value or null if parameter doesn't exist
   */
  public String[] replaceQueryParameter(String name, String value)
  {
    return _query.replaceParameter(name, value);
  }

  /**
   * Remove parameter with given name and
   * return its previous value.
   *
   * @param name parameter to remove
   * @return previous value or null if parameter doesn't exist
   */
  public String[] removeQueryParameter(String name)
  {
    return _query.removeParameter(name);
  }

  /**
   * Remove parameter with given name and
   * return its previous value.
   *
   * @param names parameter to remove
   */
  public void removeQueryParameters(String... names)
  {
    _query.removeParameters(names);
  }

  /**
   * Call this method when you want to reset the internal string to start
   * from scratch again */
  public void reset()
  {
    _scheme = null;
    _userInfo = null;
    _host = null;
    _port = -1;
    _path = null;
    _query = new QueryBuilder();
    _fragment = null;
    _escapeFragment = true;
  }

  public void resetQuery()
  {
    _query = new QueryBuilder();
  }

  /**
   * @return a url which is relative (does not contain anything before path)
   */
  @Override
  public URL createRelativeURL()
  {
    URLBuilder url = URLBuilder.createFromPath(getPath());
    url.addQuery(getQuery());
    url.setFragment(getFragment());
    return url;
  }
  /**
   * @return a new cloned object */
  public URLBuilder deepClone()
  {
    return new URLBuilder(this);
  }

  @Override
  public String toString()
  {
    return getURL();
  }

  @Override
  public boolean equals(Object o)
  {
    if(this == o) return true;
    if(o == null || getClass() != o.getClass()) return false;

    final URLBuilder that = (URLBuilder) o;

    if(_port != that._port) return false;
    if(_fragment != null ? !_fragment.equals(that._fragment) : that._fragment != null) return false;
    if(_host != null ? !_host.equals(that._host) : that._host != null) return false;
    if(_path != null ? !_path.equals(that._path) : that._path != null) return false;
    if(!_query.equals(that._query)) return false;
    if(_scheme != null ? !_scheme.equals(that._scheme) : that._scheme != null) return false;
    if(_userInfo != null ? !_userInfo.equals(that._userInfo) : that._userInfo != null) return false;

    return true;
  }

  @Override
  public int hashCode()
  {
    int result;
    result = (_scheme != null ? _scheme.hashCode() : 0);
    result = 29 * result + (_userInfo != null ? _userInfo.hashCode() : 0);
    result = 29 * result + (_host != null ? _host.hashCode() : 0);
    result = 29 * result + _port;
    result = 29 * result + (_path != null ? _path.hashCode() : 0);
    result = 29 * result + _query.hashCode();
    result = 29 * result + (_fragment != null ? _fragment.hashCode() : 0);
    return result;
  }

  /**
   * Encapsulates the call to encoding a URL so that we don't have to deal
   * with the encoding.
   *
   * @param original the string to encode
   * @return the encoded url */
  public static String urlEncode(String original)
  {
    return URLCodec.instance().urlEncode(original);
  }

  /**
   * Encapsulates the call to encoding a URL
   *
   * @param original the string to encode
   * @param encoding the encoding to use
   * @return the encoded url */
  public static String urlEncode(String original, String encoding)
    throws UnsupportedEncodingException
  {
    return new URLCodec(true, encoding).urlEncode(original);
  }

  /**
   * Encapsulates the call to decoding a URL so that we don't have to deal
   * with the encoding.
   *
   * @param original the string to decode
   * @return the encoded url */
  public static String urlDecode(String original)
  {
    return URLCodec.instance().urlDecode(original);
  }

  /**
   * Encapsulates the call to decoding a URL so that we don't have to deal
   * with the encoding.
   *
   * @param original the string to decode
   * @param encoding the encoding to use
   * @return the encoded url */
  public static String urlDecode(String original, String encoding)
    throws UnsupportedEncodingException
  {
    return new URLCodec(true, encoding).urlDecode(original);
  }

  /**
   * Factory method from a path
   *
   * @param path the path (will be url encoded!)
   * @return the builder */
  public static URLBuilder createFromPath(String path)
  {
    URLBuilder res = new URLBuilder();
    res.setPath(path);
    return res;
  }

  /**
   * Factory method from a url
   *
   * @param url the url
   * @return the builder
   * @throws URISyntaxException if the url is invalid */
  public static URLBuilder createFromURL(String url)
      throws URISyntaxException
  {
    if(url == null)
    {
      return null;
    }
    else
    {
      url = url.trim();
    }

    URI uri = new URI(url);
    if (uri.isOpaque())
      throw new URISyntaxException(url, "opaque uris not supported");

    URLBuilder res = new URLBuilder();
    res.setScheme(uri.getScheme());
    res.setUserInfo(uri.getUserInfo());

    // CA-1599 -- Java's URI fails to parse the hostname if it has underscores in it.
    // this is because _ is not a valid char in hostnames, yet there are systems out there
    // that have it. Java's URL, however, handles it fine.
    if (uri.getHost() == null && res.getHasScheme())
    {
      try
      {
        java.net.URL u = new java.net.URL(url);
        res.setHost(u.getHost());
        res.setPort(u.getPort());
      }
      catch (java.net.MalformedURLException e)
      {
        URISyntaxException uex = new URISyntaxException(url, e.getMessage());
        uex.initCause(e); // keep the original exception
        throw uex;
      }
    }
    else
    {
      res.setHost(uri.getHost());
      res.setPort(uri.getPort());
    }

    res.setPath(uri.getRawPath());
    res.setFragment(uri.getFragment());
    res.addQuery(uri);

    return res;

  }

  /**
   * Factory method from a url
   *
   * @param url the url
   * @return the builder */
  public static URLBuilder createFromURL(URL url)
  {
    if(url == null)
      return null;

    // fast implementation
    if(url instanceof URLBuilder)
    {
      URLBuilder urlBuilder = (URLBuilder) url;
      return urlBuilder.deepClone();
    }

    try
    {
      return createFromURL(url.getURL());
    }
    catch(URISyntaxException e)
    {
      throw new RuntimeException("bad url!" + url.getURL());
    }
  }

  /**
   * Convenient call which adds a query parameter to the give url.
   *
   * @param url the original URL: will stay unchanged
   * @param name the name of the query parameter
   * @param value the value of the query parameter
   * @return the new URL */
  public static URL addQueryParameter(URL url, String name, String value)
  {
    URLBuilder ub = createFromURL(url);
    ub.addQueryParameter(name, value);
    return ub;
  }

  /**
   * Returns an array of the '/' delimited components for a URL path, starting
   * just after the servlet path. For example, if requestURI is leo/a/b/c/d/
   * and servletPath is /a, then the array returned will be {b,c,d}. The path
   * components are returned decoded (not htmlified)
   * @param requestURI request URI (obtained via request.getRequestURI())
   * @param servletPath servlet path (obtained via request.getServletPath())
   * @return an array of the components in the URI, starting just after the
   * specified servletPath.
   */
  public static String[] getPathComponents(String requestURI, String servletPath)
  {
    ArrayList<String> tokens = new ArrayList<String>();

    if (requestURI != null && servletPath != null && servletPath.length() > 0)
    {
      int servletPathStartPos = requestURI.indexOf(servletPath);
      if (servletPathStartPos != -1)
      {
        String components = requestURI.substring(servletPathStartPos + servletPath.length());
        StringTokenizer st = new StringTokenizer(components, "/");
        while (st.hasMoreTokens())
        {
          tokens.add(urlDecode(st.nextToken()));
        }
      }
    }

    return tokens.toArray(new String[tokens.size()]);
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.io.resource;

import org.linkedin.util.io.resource.internal.SchemeURIResourceFactory;

import java.io.File;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;

/**
 * Basic implementation based on {@link SchemeURIResourceFactory}.
 *
 * @author ypujante@linkedin.com
 *
 */
public class URIResourceFactoryImpl implements URIResourceFactory
{
  private final Map<String, SchemeURIResourceFactory> _factories =
    new HashMap<String, SchemeURIResourceFactory>();

  /**
   * Constructor
   */
  public URIResourceFactoryImpl(SchemeURIResourceFactory... factories)
  {
    for(SchemeURIResourceFactory factory : factories)
    {
      _factories.put(factory.getScheme(), factory);
    }
  }

  /**
   * Based on the URI returns the right resource
   *
   * @param uri
   * @return the resource
   * @throws UnsupportedURIException if the uri is not supported by this factory
   */
  @Override
  public Resource createResource(URI uri) throws UnsupportedURIException
  {
    if(uri.getScheme() == null)
      return FileResource.createFromRoot(new File(uri.getPath()));

    SchemeURIResourceFactory factory = _factories.get(uri.getScheme());
    if(factory == null)
      throw new UnsupportedURIException("unsupported scheme " + uri.getScheme());

    return factory.createResource(uri, this);
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.url;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import org.linkedin.util.lang.LangUtils;
import org.linkedin.util.text.StringSplitter;
import org.linkedin.util.text.TextUtils;

/**
 * Represents the query part in the URL
 *
 * @author ypujante@linkedin.com
 */
public class QueryBuilder implements Query, Serializable
{
  private static final long serialVersionUID = 1L;

  private static StringSplitter SS = new StringSplitter('&');

  /**
   * key = name (String), value = values (String[]) */
  private Map<String, String[]> _queryParameters = null;
  private final StringBuilder _query = new StringBuilder();
  private final URLCodec _urlCodec;

  /**
   * Constructor */
  public QueryBuilder()
  {
    _urlCodec = URLCodec.instance();
  }

  /**
   * Constructor */
  public QueryBuilder(String encoding) throws UnsupportedEncodingException
  {
    // the purpose is to test right away that the encoding is supported
    // rather than later in the code
    _urlCodec = new URLCodec(true, encoding);
  }

  /**
   * Constructor */
  public QueryBuilder(URLCodec urlCodec)
  {
    _urlCodec = urlCodec;
  }

  /**
   * @return the encoding used by the query */
  @Override
  public String getEncoding()
  {
    return _urlCodec.getCharacterEncoding();
  }

  /**
   * @return true if query parameters have been added */
  @Override
  public boolean getHasQueryParameters()
  {
    return _query.length() > 0;
  }

  /**
   * @return the iterator of parameter names  */
  @Override
  public Iterator<String> getParameterNames()
  {
    return getParameterMap().keySet().iterator();
  }

  /**
   * @return the query as a string */
  @Override
  public String getQuery()
  {
    return _query.toString();
  }

  /**
   * Gets the parameters given its name
   *
   * @param name the name of the parameter
   * @return the parameters or null if none found*/
  @Override
  public String[] getParameterValues(String name)
  {
    return getMap().get(name);
  }

  /**
   * @return the parameter map */
  @Override
  public Map<String,String[]> getParameterMap()
  {
    return Collections.unmodifiableMap(getMap());
  }

  /**
   * Get the first parameter given its name
   *
   * @param name the name of the parameter
   * @return the first parameter  */
  @Override
  public String getParameter(String name)
  {
    String[] params = getParameterValues(name);
    if(params == null)
      return null;

    return params[0];
  }

  /**
   * @see org.linkedin.util.url.Query#getBooleanParameter(java.lang.String)
   */
  @Override
  public boolean getBooleanParameter(String name)
  {
    String paramValue = getParameter(name);

    if (!TextUtils.isEmptyString(paramValue) && paramValue.equals(name))
    {
      return true;
    }
    else
    {
      return LangUtils.convertToBoolean(paramValue);
    }
  }

  /**
   * @see org.linkedin.util.url.Query#getIntParameter(java.lang.String, int)
   */
  @Override
  public int getIntParameter(String name, int defaultValue)
  {
    String paramValue = getParameter(name);
    if (TextUtils.isEmptyString(paramValue))
    {
      return defaultValue;
    }
    else
    {
      try
      {
        return Integer.parseInt(paramValue);
      }
      catch (NumberFormatException e)
      {
        return defaultValue;
      }
    }
  }

  /**
   * Adds the query parameter (no value)
   *
   * @param name */
  public void addParameter(String name)
  {
    addParameter(name, "");
  }

  /**
   * Add query parameter to query string.
   *
   * @param name
   * @param value
   */
  private void addQueryParameter(String name, String value)
  {
    if(_query.length() > 0)
      _query.append('&');
    _query.append(encode(name));
    _query.append('=');
    _query.append(encode(value));
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value */
  public void addParameter(String name, String value)
  {
    addQueryParameter(name, value);

    if(_queryParameters != null)
      addParameterToMap(name, value);
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value */
  private void addParameterToMap(String name, String value)
  {
    String[] params = getParameterValues(name);
    if(params == null)
    {
      params = new String[1];
      params[0] = value;
      _queryParameters.put(name, params);
    }
    else
    {
      int len = params.length;
      String[] newParams = new String[len + 1];
      System.arraycopy(params, 0, newParams, 0, len);
      newParams[len] = value;
      _queryParameters.put(name, newParams);
    }
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param values */
  public void addParameters(String name, String[] values)
  {
    if(_query.length() > 0)
      _query.append('&');

    String encodedName = encode(name);
    for(int i = 0; i < values.length; i++)
    {
      if(i > 0)
        _query.append('&');
      _query.append(encodedName);
      _query.append('=');
      _query.append(encode(values[i]));
    }

    if(_queryParameters != null)
      addParametersToMap(name, values);
  }

  /**
   * Adds the map of parameters
   * @param parameters
   */
  public void addParameters(Map<String, String[]> parameters)
  {
    for(Map.Entry<String, String[]> entry : parameters.entrySet())
    {
      addParameters(entry.getKey(), entry.getValue());
    }
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param values */
  private void addParametersToMap(String name, String[] values)
  {
    String[] params = getParameterValues(name);
    if(params == null)
    {
      _queryParameters.put(name, values);
    }
    else
    {
      int len = params.length;
      String[] newParams = new String[len + values.length];
      System.arraycopy(params, 0, newParams, 0, len);
      System.arraycopy(values, 0, newParams, len, values.length);
      _queryParameters.put(name, newParams);
    }
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value */
  public void addParameter(String name, int value)
  {
    addParameter(name, String.valueOf(value));
  }

  /**
   * Adds the query parameter
   *
   * @param name
   * @param value */
  public void addParameter(String name, long value)
  {
    addParameter(name, String.valueOf(value));
  }

  /**
   * Adds a parameter that comes from an array at the provided index
   *
   * @param name the name of the parameter
   * @param value the value
   * @param index the index in the array
   */
  public void addIndexedParameter(String name, String value, int index)
  {
    addParameter(getIndexedParamName(name, index), value);
  }

  /**
   * Adds a parameter that comes from an array at the provided index (can have multiple dimensions!)
   *
   * @param name the name of the parameter
   * @param value the value
   * @param indices the indices in the array (multiple dimensions)
   */
  public void addIndexedParameter(String name, String value, int... indices)
  {
    addParameter(getIndexedParamName(name, indices), value);
  }

  /**
   * Adds a parameter that comes from an array at the provided index
   *
   * @param name the name of the parameter
   * @param value the value
   * @param index the index in the array
   */
  public void addIndexedParameter(String name, int value, int index)
  {
    addParameter(getIndexedParamName(name, index), value);
  }

  /**
   * Adds a parameter that comes from an array at the provided index (can have multiple dimensions!)
   *
   * @param name the name of the parameter
   * @param value the value
   * @param indices the indices in the array (multiple dimensions)
   */
  public void addIndexedParameter(String name, int value, int... indices)
  {
    addParameter(getIndexedParamName(name, indices), value);
  }

  /**
   * Adds the query parameter. If value is true then
   * adds the name as the value, else don't add it at all.
   *
   * @param name
   * @param value */
  public void addBooleanParameter(String name, boolean value)
  {
    if(value)
      addParameter(name, name);
  }

  /**
   * Simply adds the query provided. It is assumed that the query is properly
   * encoded for url!
   *
   * @param query the properly url encoded query (null is ok) */
  public void addQuery(String query) throws URISyntaxException
  {
    addQuery(query, true);
  }

  /**
   * Adds the query extracted from the URI. No Exception is thrown because the
   * uri could not be built if it was not correct.
   *
   * @param uri  */
  public void addQuery(URI uri)
  {
    try
    {
      addQuery(uri.getRawQuery(), false);
    }
    catch(URISyntaxException e)
    {
      // should not happen!
      throw new RuntimeException(e);
    }
  }
  /**
   * Simply adds the query provided. It is assumed that the query is properly
   * encoded for url!
   *
   * @param query the properly url encoded query (null is ok)
   * @param validate true true for validating (should be called
   * with false only when sure that it is valid..) */
  private void addQuery(String query, boolean validate) throws URISyntaxException
  {
    if(query == null)
      return;

    if("".equals(query))
      return;

    if(validate)
      validateQuery(query);

    if(_query.length() > 0)
      _query.append('&');

    _query.append(query);

    if(_queryParameters != null)
      addQueryToMap(query);
  }

  /**
   * Internal method in charge of validating the query provided. Exception
   * when query is not valid.
   *
   * @param query the query to validate
   * @throws URISyntaxException if query invalid */
  private void validateQuery(String query) throws URISyntaxException
  {
    if(query.length() == 0)
      return;

    Iterator<String> iter = SS.splitToIterator(query);

    while(iter.hasNext())
    {
      String s = iter.next();
      if(s.length() > 0)
      {
        int idx = s.indexOf('=');
        if(idx == -1)
          throw new URISyntaxException(query, "missing equal sign in " + s);
        if(s.lastIndexOf('=') != idx)
          throw new URISyntaxException(query, "extra equal sign in " + s);
      }
    }
  }

  /**
   * Simply adds the query provided. It is assumed that the query is properly
   * encoded for url!
   *
   * @param query the properly url encoded query */
  private void addQueryToMap(String query)
  {
    String[] params = SS.split(query);
    for(int i = 0; i < params.length; i++)
    {
      String param = params[i];
      if(param.length() == 0)
        continue;

      int idx = param.indexOf('=');
      if(idx == -1)
        addParameterToMap(decode(param), null);
      else
        addParameterToMap(decode(param.substring(0, idx)),
            decode(param.substring(idx + 1)));
    }
  }

  /**
   * Simply adds the query provided.
   *
   * @param query the query (null is ok) */
  public void addQuery(Query query)
  {
    if(query == null)
      return;
    try
    {
      if(!query.getEncoding().equals(getEncoding()))
        throw new RuntimeException("TODO");

      addQuery(query.getQuery(), false);
    }
    catch(URISyntaxException e)
    {
      // shouldn't happen since a query is already properly formatted
      throw new RuntimeException(e);
    }
  }

  /**
   * Call this method when you want to reset the internal string to start
   * from scratch again */
  public void reset()
  {
    _query.setLength(0);
    _queryParameters = null;
  }

  /**
   * @return as a string   */
  @Override
  public String toString()
  {
    return getQuery();
  }

  @Override
  public boolean equals(Object o)
  {
    if(this == o) return true;
    if(o == null || getClass() != o.getClass()) return false;

    final QueryBuilder that = (QueryBuilder) o;

    if(!_urlCodec.equals(that._urlCodec)) return false;
    if(!_query.toString().equals(that._query.toString())) return false;

    return true;
  }

  @Override
  public int hashCode()
  {
    int result;
    result = _query.hashCode();
    result = 29 * result + _urlCodec.hashCode();
    return result;
  }

  /**
   * Internal call that will return the map. If null then first
   * populate it
   * @return the map  */
  private Map<String,String[]> getMap()
  {
    if(_queryParameters != null)
      return _queryParameters;

    _queryParameters = new LinkedHashMap<String,String[]>();
    addQueryToMap(_query.toString());

    return _queryParameters;
  }

  /**
   * @return a new cloned object */
  public QueryBuilder deepClone()
  {
    QueryBuilder query = new QueryBuilder(_urlCodec);
    query.addQuery(this);
    return query;
  }

  /**
   * Encodes the string
   *
   * @param s
   * @return the string encoded */
  private String encode(String s)
  {
    return _urlCodec.urlEncode(s);
  }

  /**
   * Decodes the string
   *
   * @param s
   * @return the string decoded */
  private String decode(String s)
  {
    return _urlCodec.urlDecode(s);
  }

  /**
   * Returns the name of the param that represent an entry in an array. This is the method
   * used by addIndexed* methods.
   *
   * @param paramPrefix the prefix for the param
   * @param index the index in the array
   * @return the name of the param that represent an entry in an array
   */
  public static String getIndexedParamName(String paramPrefix, int index)
  {
    StringBuilder sb = new StringBuilder(paramPrefix);
    sb.append('_').append(index);

    return sb.toString();
  }

  /**
   * Returns the name of the param that represent an entry in an array. This is the method
   * used by addIndexed* methods.
   *
   * @param paramPrefix the prefix for the param
   * @param indices the indices in the array (multiple dimensions)
   * @return the name of the param that represent an entry in an array
   */
  public static String getIndexedParamName(String paramPrefix, int... indices)
  {
    StringBuilder sb = new StringBuilder(paramPrefix);
    for(int index : indices)
    {
      sb.append('_').append(index);
    }

    return sb.toString();
  }

  /**
   * Rebuild query instance with values in the map.
   */
  private void rebuildQuery()
  {
    Map<String, String[]> map = getMap();

    // reset query instance and re-populate it again
    _query.setLength(0);
    for (String key : map.keySet())
    {
      String[] parameters = map.get(key);
      for (String param : parameters)
      {
        addQueryParameter(key, param);
      }
    }
  }

  /**
   * Remove parameter with given name and
   * return its previous value.
   *
   * @param parameterNames
   */
  public void removeParameters(String... parameterNames)
  {
    Map<String, String[]> map = getMap();
    boolean needsRebuild = false;
    for (String name : parameterNames)
    {
      if(map.remove(name) != null)
        needsRebuild = true;
    }
    if(needsRebuild)
      rebuildQuery();
  }

  /**
   * Remove parameter with given name and
   * return its previous value.
   *
   * @param name parameter to remove
   * @return previous value or null if parameter doesn't exist
   */
  public String[] removeParameter(String name)
  {
    Map<String, String[]> map = getMap();
    String[] v = map.remove(name);
    if(v != null)
      rebuildQuery();
    return v;
  }

  /**
   * Replace existing or add new parameter with given
   * value. Any existing parameter values are
   * deleted prior to adding the new value.
   *
   * @param name parameter to replace
   * @return previous values or null if parameter doesn't exist
   */
  public String[] replaceParameter(String name, String value)
  {
    Map<String, String[]> map = getMap();
    String[] v = map.put(name, new String[] { value });
    rebuildQuery();
    return v;
  }

}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.io.resource.internal;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;

import org.linkedin.util.clock.Timespan;
import org.linkedin.util.io.PathUtils;
import org.linkedin.util.io.resource.ResourceFilter;
import org.linkedin.util.io.resource.URLResource;
import org.linkedin.util.io.resource.UnsupportedURIException;

/**
 * @author ypujante@linkedin.com
 *
 */
public class URLResourceProvider extends PathBasedResourceProvider
{
  private final URL _baseURL;
  private final Timespan _connectTimeout;
  private final Timespan _readTimeout;

  /**
   * Constructor
   */
  public URLResourceProvider(URL baseURL) throws UnsupportedURIException, URISyntaxException
  {
    this(baseURL, "/");
  }

  /**
   * Constructor
   */
  public URLResourceProvider(URL baseURL, Timespan connectTimeout, Timespan readTimeout) throws UnsupportedURIException, URISyntaxException
  {
    this(baseURL, "/", connectTimeout, readTimeout);
  }

  /**
   * Constructor
   */
  public URLResourceProvider(URL baseURL, String root) throws UnsupportedURIException, URISyntaxException
  {
    this(baseURL, root, URLResource.DEFAULT_CONNECT_TIMEOUT, URLResource.DEFAULT_READ_TIMEOUT);
  }

  /**
   * Constructor
   */
  public URLResourceProvider(URL baseURL, String root, Timespan connectTimeout, Timespan readTimeout) throws UnsupportedURIException, URISyntaxException
  {
    super(PathUtils.addPaths(baseURL.getPath(), root));

    if(baseURL.toURI().isOpaque())
      throw new UnsupportedURIException(baseURL + " is opaque");

    _baseURL = baseURL;
    _connectTimeout = connectTimeout;
    _readTimeout = readTimeout;
  }

  /**
   * Creates a new resource provider given the new path.
   *
   * @param rootPath
   * @return the new resource provider
   */
  @Override
  public InternalResourceProvider doCreateResourceProvider(String rootPath)
  {
    try
    {
      return new URLResourceProvider(_baseURL, getFullPath(rootPath));
    }
    catch(UnsupportedURIException e)
    {
      throw new RuntimeException(e);
    }
    catch(URISyntaxException e)
    {
      throw new RuntimeException(e);
    }
  }

  /**
   * Builds a resource given a path. Only subclasses know how to do that.
   *
   * @param path the path to the new resource (always starts with /)
   * @return the resource
   */
  @Override
  public InternalResource doBuildResource(String path)
  {
    try
    {
      URI uri = _baseURL.toURI();

      URI newURI = new URI(uri.getScheme(),
                           uri.getUserInfo(),
                           uri.getHost(),
                           uri.getPort(),
                           getFullPath(path),
                           null,
                           null);

      return new URLResource(this, path, newURI.toURL(), _connectTimeout, _readTimeout);
    }
    catch(URISyntaxException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }
    catch(MalformedURLException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }
  }

  /**
   * If the path denotes a directory, then it will return all resources that are contained in the
   * directory.
   *
   * @param path   the path to the resource (it already ends with /)
   * @param filter the filter to include only some resources in the result
   * @return true if it was a directory, false otherwise
   */
  @Override
  public boolean doList(String path, ResourceFilter filter)
  {
    return false;
  }

  @Override
  public boolean equals(Object o)
  {
    if(this == o) return true;
    if(o == null || getClass() != o.getClass()) return false;
    if(!super.equals(o)) return false;

    URLResourceProvider that = (URLResourceProvider) o;

    if(!_baseURL.equals(that._baseURL)) return false;

    return true;
  }

  @Override
  public int hashCode()
  {
    int result = super.hashCode();
    result = 31 * result + _baseURL.hashCode();
    return result;
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.io.resource.internal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.linkedin.util.io.PathUtils;
import org.linkedin.util.io.resource.JarResource;
import org.linkedin.util.io.resource.Resource;
import org.linkedin.util.io.resource.ResourceFilter;

import java.io.IOException;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author ypujante@linkedin.com
 *
 */
public class JarResourceProvider extends PathBasedResourceProvider
{
  public static final String MODULE = JarResourceProvider.class.getName();
  public static final Logger log = LoggerFactory.getLogger(MODULE);

  private final LeafResource _jarResource;

  /**
   * Constructor
   */
  public JarResourceProvider(Resource jarResource)
  {
    this(jarResource, "/");
  }

  /**
   * Constructor
   */
  public JarResourceProvider(Resource jarResource, String root)
  {
    super(root);
    // we need to have access to a File no matter what...
    _jarResource = LocalCacheLeafResource.create(jarResource);
  }

  /**
   * Constructor
   */
  private JarResourceProvider(LeafResource jarResource, String root)
  {
    super(root);
    _jarResource = jarResource;
  }

  /**
   * Creates a new resource provider given the new path.
   *
   * @param rootPath
   * @return the new resource provider
   */
  @Override
  public InternalResourceProvider doCreateResourceProvider(String rootPath)
  {
    return new JarResourceProvider(_jarResource, getFullPath(rootPath));
  }

  /**
   * Builds a resource given a path. Only subclasses know how to do that.
   *
   * @param path the path to the new resource (always starts with /)
   * @return the resource
   */
  @Override
  public InternalResource doBuildResource(String path)
  {
    return new JarResource(this, path, _jarResource, getFullPath(path));
  }

  /**
   * If the path denotes a directory, then it will return all resources that are contained in the
   * directory.
   *
   * @param path   the path to the resource (it already ends with /)
   * @param filter the filter to include only some resources in the result
   * @return true if it was a directory, false otherwise
   */
  @Override
  public boolean doList(String path, ResourceFilter filter)
  {
    try
    {
      JarFile jarFile = new JarFile(_jarResource.getFile());
      try
      {
        return doList(path, filter, jarFile);
      }
      finally
      {
        jarFile.close();
      }
    }
    catch(IOException e)
    {
      if(log.isDebugEnabled())
        log.debug("exception (ignored) while listing path " + path, e);

      return false;
    }
  }

  private boolean doList(String path, ResourceFilter filter, JarFile jarFile)
  {
    String directory = PathUtils.removeLeadingSlash(getFullPath(path));

    boolean isDirectory = false;

    // we iterate over ALL entries in the jar file (unfortunately there is no other api to
    // do that :( ).
    Enumeration<JarEntry> iter = jarFile.entries();
    while(iter.hasMoreElements())
    {
      JarEntry entry = iter.nextElement();
      String entryName = entry.getName();

      // we found the root entry => we know for sure it is a directory! but we don't add it to
      // the resultset
      if(entryName.equals(directory))
      {
        isDirectory = true;
        continue;
      }

      if(entryName.startsWith(directory))
      {
        int idx = entryName.indexOf("/", directory.length());
        // if it is a simple file or a subdir then accept it
        if(idx == -1 || idx == (entryName.length() - 1))
        {
          isDirectory = true;
          Resource resource = new JarResource(this,
                                              getRelativePath(PathUtils.addLeadingSlash(entryName)),
                                              _jarResource,
                                              entryName);

          filter.accept(resource);
        }
      }
    }

    return isDirectory;
  }

  @Override
  public boolean equals(Object o)
  {
    if(this == o) return true;
    if(o == null || getClass() != o.getClass()) return false;
    if(!super.equals(o)) return false;

    JarResourceProvider that = (JarResourceProvider) o;

    if(!_jarResource.equals(that._jarResource)) return false;

    return true;
  }

  @Override
  public int hashCode()
  {
    int result = super.hashCode();
    result = 31 * result + _jarResource.hashCode();
    return result;
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.lifecycle;

import org.slf4j.Logger;
import org.linkedin.util.annotations.Initializer;
import org.linkedin.util.reflect.ObjectProxy;
import org.linkedin.util.reflect.ReflectUtils;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Checks for shutdown then dispatches the call and let Exception go through and logs
 * a message for all other exceptions.
 *
 * @author ypujante@linkedin.com
 *
 */
public class ShutdownProxy implements InvocationHandler, Serializable, ObjectProxy
{
  private static final long serialVersionUID = 1L;

  public static final String MODULE = ShutdownProxy.class.getName();
  public static final Logger log = org.slf4j.LoggerFactory.getLogger(MODULE);

  private Object _object;
  private Shutdown _shutdown;

  /**
   * Constructor
   */
  public ShutdownProxy(Object object, Shutdown shutdown)
  {
    _object = object;
    _shutdown = shutdown;
  }

  /**
   * Constructor
   */
  public ShutdownProxy()
  {
  }

  public Object getObject()
  {
    return _object;
  }

  @Initializer(required = true)
  public void setObject(Object object)
  {
    _object = object;
  }

  public Shutdown getShutdown()
  {
    return _shutdown;
  }

  @Initializer(required = true)
  public void setShutdown(Shutdown shutdown)
  {
    _shutdown = shutdown;
  }

  /**
   * @return the object proxied
   */
  @Override
  public Object getProxiedObject()
  {
    return _object;
  }

  /**
   * Method from the interface. Checks for shutdown then dispatches the call
   *
   * @param o
   * @param method
   * @param objects
   * @return object
   * @throws Throwable
   */
  @Override
  public Object invoke(Object o, Method method, Object[] objects)
    throws Throwable
  {
    // throws ShutdownRequestedException when in shutdown mode
    _shutdown.startCall();
    try
    {
      return method.invoke(_object, objects);
    }
    catch(InvocationTargetException e)
    {
      Throwable th = e.getTargetException();

      try
      {
        throw th;
      }
      catch(Exception ex)
      {
        throw ex;
      }
      catch(Throwable throwable)
      {
        log.error(method.toString(), throwable);
        throw throwable;
      }
    }
    finally
    {
      _shutdown.endCall();
    }
  }
  /**
   * Creates the proxy to check for shutdown. Uses all interfaces defined by
   * this object (and recursively).
   *
   * @param o
   * @param shutdown
   * @return the proxy */
  public static Object createShutdownProxy(Object o,
                                           Shutdown shutdown)
  {
    return createShutdownProxy(o,
                               null,
                               shutdown);

  }

  /**
   * Creates the proxy to check for shutdown.
   *
   * @param o
   * @param interfaces restriction on which interface to apply the proxy on
   * @param shutdown
   * @return the proxy */
  public static Object createShutdownProxy(Object o,
                                           Class[] interfaces,
                                           Shutdown shutdown)
  {
    if(interfaces == null)
      interfaces = ReflectUtils.extractAllInterfaces(o);

    return Proxy.newProxyInstance(o.getClass().getClassLoader(),
                                  interfaces,
                                  new ShutdownProxy(o, shutdown));

  }

}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.lang;

import org.linkedin.util.exceptions.InternalException;
import org.linkedin.util.io.IOUtils;
import org.linkedin.util.text.IdentityString;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.Date;

/**
 * Contains useful global utils methods.
 *
 * @author ypujante@linkedin.com */
public class LangUtils
{
  /*
   * @param o the object you want the identity string (note that null is ok)
   * @return a string representing the identity of an object which is what o.toString()
   * would return if both {@link Object#toString()} and {@link Object#hashCode()} were not overriden.
   */
  public static String identityString(Object o)
  {
    return IdentityString.FULL_IDENTITY_STRING.getIdentity(o);
  }

  /**
   * This version returns a short version of the class name
   *
   * @param o the object you want the identity string (note that null is ok)
   * @return a string representing the identity of an object which is what o.toString()
   *         would return if both {@link Object#toString()} and {@link Object#hashCode()} were not
   *         overriden.
   */
  public static String shortIdentityString(Object o)
  {
    return IdentityString.SHORT_IDENTITY_STRING.getIdentity(o);
  }

  /**
   * Compares 2 values. Calls compareTo when they are not null
   *
   * @param o1
   * @param o2
   * @return the result of compareTo */
  public static <T extends Comparable<T>> int compare(T o1, T o2)
  {
    if(o1 == null)
    {
      if(o2 == null)
        return 0;
      else
        return -1;
    }

    if(o2 == null)
    {
      // here d1 is not null (already tested..)
      return 1;
    }

    return o1.compareTo(o2);
  }

  /**
   * Equivalent to o1.equals(o2) but handle null properly
   */
  public static boolean isEqual(Object o1, Object o2)
  {
    if(o1 == null)
      return o2 == null;

    if(o2 == null)
      return false;

    return o1.equals(o2);
  }

  /**
   * Compares 2 ints. The purpose of this method is to return 0,1,-1 depending on how
   * o1 and o2 compares
   *
   * @param o1
   * @param o2
   * @return 0 if o1==o2, -1 if o1 < o2, 1, if o1 > o2 */
  public static int compare(int o1, int o2)
  {
    if(o1 == o2)
      return 0;

    if(o1 < o2)
      return -1;

    return 1;
  }

  /**
   * Compares 2 longs. The purpose of this method is to return 0,1,-1 depending on how
   * o1 and o2 compares
   *
   * @param o1
   * @param o2
   * @return 0 if o1==o2, -1 if o1 < o2, 1, if o1 > o2 */
  public static int compare(long o1, long o2)
  {
    if(o1 == o2)
      return 0;

    if(o1 < o2)
      return -1;

    return 1;
  }

  /**
   * Compares 2 floats. The purpose of this method is to return 0,1,-1 depending on how
   * o1 and o2 compares
   *
   * @param o1
   * @param o2
   * @return 0 if o1==o2, -1 if o1 < o2, 1, if o1 > o2 */
  public static int compare(float o1, float o2)
  {
    if(o1 == o2)
      return 0;

    if(o1 < o2)
      return -1;

    return 1;
  }

  /**
   * This method compares 2 dates. It is mainly written to account for a bug
   * in jdk15 which throws a ClassCastException when calling d1.compareTo(d2).
   * It also adds handling of null values.
   *
   * @param d1
   * @param d2
   * @return 0 if equal, < 0 if d1 < d2 and > 0 if d1 > d2
   */
  public static int compareTo(Date d1, Date d2)
  {
    if(d1 == null)
    {
      if(d2 == null)
        return 0;
      else
        return -1;
    }

    if(d2 == null)
    {
      // here d1 is not null (already tested..)
      return 1;
    }

    // here d1 and d2 are not null
    long delta = d1.getTime() - d2.getTime();

    if (delta > 0)
    {
      return 1;
    }
    else if (delta < 0)
    {
      return -1;
    }
    else
    {
      return 0;
    }
  }

  /**
   * Clone by serializing / deserializing... only works if the object is actually serializable!
   */
  @SuppressWarnings("unchecked")
  public static <T extends Serializable> T deepClone(T serializable)
  {
    if(serializable == null)
      return null;

    try
    {
      return (T) IOUtils.deserialize(IOUtils.serialize(serializable));
    }
    catch(IOException e)
    {
      throw new InternalException(e);
    }
    catch(ClassNotFoundException e)
    {
      throw new InternalException(e);
    }
  }

  /**
   * Clone by serializing / deserializing... only works if the object is actually serializable!
   */
  @SuppressWarnings("unchecked")
  public static <T extends Serializable> T deepClone(T serializable, ClassLoader classLoader)
  {
    if(serializable == null)
      return null;

    try
    {
      return (T) IOUtils.deserialize(IOUtils.serialize(serializable), classLoader);
    }
    catch(IOException e)
    {
      throw new InternalException(e);
    }
    catch(ClassNotFoundException e)
    {
      throw new InternalException(e);
    }
  }

  /**
   * Converts the object into a boolean value.
   *
   * @param o the object to convert
   * @return a boolean  */
  public static boolean convertToBoolean(Object o)
  {
    if(o == null)
      return false;

    if(o instanceof Boolean)
    {
      return (Boolean) o;
    }

    return convertToBoolean(o.toString());
  }

  /**
   * Converts the string into a boolean value. true or
   * yes are considered to be true. false
   * or no are false.
   *
   * @param s the string to convert
   * @return a boolean  */
  public static boolean convertToBoolean(String s)
  {
    if(s == null)
      return false;

    if(s.equals("false") || s.equals("no") || s.equals("off"))
      return false;

//    if(s.equals("true") || s.equals("yes") || s.equals("on"))
//      return true;

    return true;
  }

  /**
   * Returns the stack trace of the throwable as a string
   *
   * @param th the throwable
   * @return the stack trace as a string */
  public static String getStackTrace(Throwable th)
  {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    th.printStackTrace(pw);
    return sw.toString();
  }
}


/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.util.io;

/**
 * Utility methods for path related computation.
 *
 * @author ypujante@linkedin.com
 *
 */
public class PathUtils
{
  /**
   * Adds 2 paths taking into consideration the /
   *
   * @param path1
   * @param path2
   * @return path1 + path2
   */
  public static String addPaths(String path1, String path2)
  {
    if(path1.endsWith("/"))
    {
      if(path2.startsWith("/"))
        return path1.substring(0, path1.length() - 1) + path2;
      else
        return path1 + path2;
    }
    else
    {
      if(path2.startsWith("/"))
        return path1 + path2;
      else
        return path1 + "/" + path2;
    }
  }

  /**
   * Adds a leading slash if not already exists
   *
   * @param path
   * @return the path with a leading /
   */
  public static String addLeadingSlash(String path)
  {
    if(!path.startsWith("/"))
      return "/" + path;
    else
      return path;
  }

  /**
   * Removes a leading slash if exists
   *
   * @param path
   * @return the path without a leading /
   */
  public static String removeLeadingSlash(String path)
  {
    if(path.startsWith("/"))
      return path.substring(1);
    else
      return path;
  }

  /**
   * Adds a trailing slash if not already exists
   *
   * @param path
   * @return the path with a trailing /
   */
  public static String addTrailingSlash(String path)
  {
    if(!path.endsWith("/"))
      return path + "/";
    else
      return path;
  }

  /**
   * Removes a trailing slash if exists
   *
   * @param path
   * @return the path without a trailing /
   */
  public static String removeTrailingSlash(String path)
  {
    if(path.endsWith("/"))
      return path.substring(0, path.length() - 1);
    else
      return path;
  }

  /**
   * @param path
   * @return the parent path given a path
   */
  public static String getParentPath(String path)
  {
    String parentPath = path;

    if("/".equals(parentPath))
      return parentPath;

    parentPath = PathUtils.removeTrailingSlash(parentPath);

    int idx = parentPath.lastIndexOf("/");
    if(idx >= 0)
      parentPath = parentPath.substring(0, idx + 1);
    else
      parentPath = "/";

    return parentPath;
  }

  /**
   * Constructor
   */
  public PathUtils()
  {
  }
}