/*
 * 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.text;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
import java.util.List;
import java.util.Collections;

/**
 * This is a utility class to split up a string
 *
 * @author ypujante@linkedin.com */
public class StringSplitter
{
  private final char _delim;

  private final String _blockIgnore;

  /**
   * Constructor
   *
   * @param delim the character to use as a delimiter for splitting */
  public StringSplitter(char delim)
  {
    this(delim, null);
  }

  /**
   * Constructor. This one takes another character that will be used to
   * ignore delim. For example:
   * "param1=value1&param2='toto=3'&param3=value3". In this
   * string the delimiter = should be ignored within the block
   * delimited by '.
   *
   * @param delim the character to use as a delimiter for splitting
   * @param blockIgnore the character to use to ignore delim within it */
  public StringSplitter(char delim, char blockIgnore)
  {
    this(delim, String.valueOf(blockIgnore));
  }

  /**
   * Constructor. This one takes another character that will be used to
   * ignore delim. For example:
   * "param1=value1&param2='toto=3'&param3=value3". In this
   * string the delimiter = should be ignored within the block
   * delimited by '.
   *
   * @param delim the character to use as a delimiter for splitting
   * @param blockIgnore the character to use to ignore delim within it */
  public StringSplitter(char delim, String blockIgnore)
  {
    _delim = delim;
    if(blockIgnore != null && blockIgnore.length() == 0)
      throw new IllegalArgumentException("block ignore cannot be the empty string");
    _blockIgnore = blockIgnore;
  }

  /**
   * Splits the string into an array of strings using the delimiter
   * provided in the constructor.
   *
   * @param s the string to split
   * @return the splitted string (null if s is
   *         null) */
  public String[] split(String s)
  {
    List<String> ss = splitAsList(s);
    if(ss == null)
      return null;

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

  /**
   * Splits the string into an array of strings using the delimiter
   * provided in the constructor.
   *
   * @param s the string to split
   * @return the splitted string (null if s is
   *         null) */
  public List<String> splitAsList(String s)
  {
    if(s == null)
      return null;

    int len = s.length();

    if(len == 0)
    {
      return Collections.emptyList();
    }

    if(_blockIgnore == null)
      return splitNoBlockIgnore(s);
    else
      return splitBlockIgnore(s);
  }

  /**
   * Splits the string into an array of strings using the delimiter
   * provided in the constructor.
   *
   * @param s the string to split
   * @return the set of string (null if s is
   *         null) */
  public Set<String> splitAsSet(String s)
  {
    String[] ss = split(s);
    if(ss == null)
      return null;

    Set<String> res = new TreeSet<String>();
    for(int i = 0; i < ss.length; i++)
      res.add(ss[i]);

    return res;
  }

  /**
   * Splits the string and return an iterator using the delimiter
   * provided in the constructor.
   *
   * @param s the string to split
   * @return iterator of String (null if s is
   *         null) */
  public Iterator<String> splitToIterator(String s)
  {
    if(s == null)
      return null;

    if(_blockIgnore == null)
      return new SplitNoBlockIgnoreIterator(s);
    else
      throw new RuntimeException("TODO");
  }

  /**
   * Called when no block ignore */
  private List<String> splitNoBlockIgnore(String s)
  {
    int len = s.length();

    ArrayList<String> res = new ArrayList<String>();

    int idx = 0;
    int prev = 0;

    while(true)
    {
      idx = s.indexOf(_delim, idx);

      if(idx == -1)
      {
        res.add((prev == len) ? "" : s.substring(prev));
        break;
      }

      if(prev == idx)
      {
        res.add("");
      }
      else
      {
        res.add(s.substring(prev, idx));
      }

      prev = ++idx;
    }

    return res;
  }

  /**
   * The iterator */
  private class SplitNoBlockIgnoreIterator implements Iterator<String>
  {
    private final String _s;
    private int _idx = 0;
    private int _prev = 0;
    private String _next = null;

    private SplitNoBlockIgnoreIterator(String s)
    {
      _s = s;
      if(_s.length() == 0)
      {
        _next = null;
        _idx = -1;
      }
      else
        _next = advanceNext();
    }

    @Override
    public boolean hasNext()
    {
      return _next != null;
    }

    @Override
    public String next()
    {
      String next = _next;
      if(next == null)
        throw new NoSuchElementException();
      _next = advanceNext();
      return next;
    }

    private String advanceNext()
    {
      if(_idx == -1)
        return null;

      _idx = _s.indexOf(_delim, _idx);

      if(_idx == -1)
        return (_prev == _s.length()) ? "" : _s.substring(_prev);

      try
      {
        if(_prev == _idx)
          return "";
        else
          return _s.substring(_prev, _idx);
      }
      finally
      {
        _prev = ++_idx;
      }
    }

    @Override
    public void remove()
    {
      throw new UnsupportedOperationException("not supported!");
    }
  }

  /**
   * Called when block ignore */
  private List<String> splitBlockIgnore(String s)
  {
    if(s.indexOf(_blockIgnore) == -1)
      // if the character is not present then we can call the other method
      return splitNoBlockIgnore(s);

    boolean inBlockIgnore = false;
    int blockIgnoreIdx = 0;
    StringBuffer sb = new StringBuffer();
    ArrayList<String> al = new ArrayList<String>();

    int len = s.length();

    for(int i = 0; i < len; i++)
    {
      char c = s.charAt(i);
      if(c == _blockIgnore.charAt(blockIgnoreIdx))
      {
        blockIgnoreIdx++;
        if(_blockIgnore.length() == blockIgnoreIdx)
        {
          blockIgnoreIdx = 0;
          inBlockIgnore = !inBlockIgnore;
        }
        sb.append(c);
      }
      else
      {
        blockIgnoreIdx = 0;
        if(c == _delim)
        {
          if(inBlockIgnore)
            sb.append(c);
          else
          {
            al.add(sb.toString());
            sb = new StringBuffer();
          }
        }
        else
          sb.append(c);
      }
    }

    al.add(sb.toString());

//     if(inBlockIgnore)
//       throw new IllegalArgumentException("Wrong block ignore in string " + s);

    return al;
  }
}


/*
 * 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.resource.NullResource;
import org.linkedin.util.io.resource.ResourceFilter;

/**
 * Implements the null pattern for resource (does not exist)
 * @author ypujante@linkedin.com
 */
public class NullResourceProvider extends PathBasedResourceProvider
{

  /**
   * Constructor
   */
  public NullResourceProvider()
  {
    this("/");
  }

  /**
   * Constructor
   */
  public NullResourceProvider(String root)
  {
    super(normalizePath(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 NullResource(this, normalizePath(path), normalizePath(getFullPath(path)));
  }

  /**
   * The path never represents a directory => should not end with /
   */
  private static String normalizePath(String path)
  {
    path = PathUtils.removeTrailingSlash(path);
    path = PathUtils.addLeadingSlash(path);
    return 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 NullResourceProvider(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
   */
  @Override
  public boolean doList(String path, ResourceFilter filter)
  {
    return false;
  }
}


/*
 * 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.resource.Resource;
import org.linkedin.util.io.resource.URIResourceFactory;

import java.net.URI;

/**
 * @author ypujante@linkedin.com
 *
 */
public abstract class SchemeURIResourceFactory
{
  /**
   * Constructor
   */
  public SchemeURIResourceFactory()
  {
  }

  /**
   * @return the scheme that this factory handles
   */
  public abstract String getScheme();

  /**
   * Based on the URI returns the right resource
   *
   * @param uri
   * @return the resource
   */
  public Resource createResource(URI uri, URIResourceFactory parent)
  {
    if(!getScheme().equals(uri.getScheme()))
      throw new IllegalArgumentException("cannot handle: " + uri.getScheme());

    return doCreateResource(uri, parent);
  }

  /**
   * Method that needs to be implemented by children..
   *
   * @param uri
   * @param parent the parent resource factory
   * @return the resource
   */
  protected abstract Resource doCreateResource(URI uri, URIResourceFactory parent);
}


/*
 * 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.linkedin.util.annotations.Initializable;
import org.linkedin.util.clock.Timespan;
import org.linkedin.util.concurrent.WaitableCounter;

import java.io.Serializable;
import java.util.concurrent.TimeoutException;

/**
 * Simple object that has the knowledge wether we are in shutdown or not. Works in close relation
 * with {@link ShutdownProxy}.
 *
 * @author ypujante@linkedin.com
 *
 */
public class Shutdown implements Serializable, Shutdownable
{
  private static final long serialVersionUID = 1L;

  private boolean _shutdown = false;

  @Initializable
  public WaitableCounter pendingCallsCount = new WaitableCounter();

  @Initializable
  public String module = Shutdown.class.getName();

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

  /**
   * Sets the system is shutdown */
  @Override
  public synchronized void shutdown()
  {
    _shutdown = true;
  }

  /**
   * Waits for shutdown to be completed. After calling shutdown, there may still be some pending
   * work that needs to be accomplised. This method will block until it is done.
   *
   * @throws InterruptedException  if interrupted while waiting
   * @throws IllegalStateException if shutdown has not been called
   */
  @Override
  public void waitForShutdown() throws InterruptedException, IllegalStateException
  {
    if(!_shutdown) throw new IllegalStateException("call shutdown first");
    pendingCallsCount.waitForCounter();
  }

  /**
   * Waits for shutdown to be completed. After calling shutdown, there may still be some pending work
   * that needs to be accomplised. This method will block until it is done but no longer than the
   * timeout.
   *
   * @param timeout how long to wait maximum for the shutdown
   * @throws InterruptedException  if interrupted while waiting
   * @throws IllegalStateException if shutdown has not been called
   * @throws TimeoutException      if shutdown still not complete after timeout
   */
  @Override
  public void waitForShutdown(Object timeout)
    throws InterruptedException, IllegalStateException, TimeoutException
  {
    if(!_shutdown) throw new IllegalStateException("call shutdown first");
    pendingCallsCount.waitForCounter(timeout);
  }

  /**
   * Called right before executing a call
   *
   * @throws ShutdownRequestedException
   */
  synchronized void startCall() throws ShutdownRequestedException
  {
    if(_shutdown)
      throw new ShutdownRequestedException(module);

    pendingCallsCount.inc();
  }

  /**
   * MUST be called if {@link #startCall()} is called after the call (typically in a
   * finally)
   */
  void endCall()
  {
    pendingCallsCount.dec();
  }

  /**
   * @return the number of currently pending calls
   */
  public int getPendingCallsCount()
  {
    return pendingCallsCount.getCounter();
  }
}


/*
 * 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.xml;

import java.util.Map;
import java.util.Iterator;

/**
 * Implements the XMLBuilder interface to create XML documents using a
 * StringBuffer. This should be much more lightweight than using
 * the DOM API.
 *
 * @author  ypujante@linkedin.com
 * @see XMLBuilder */
public class XML extends AbstractXMLBuilder
{
  /**
   * Contains the xml document as it is generated */
  private final StringBuilder _xml = new StringBuilder();

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

  /**
   * Adds the XML declaration (<?xml version="1.0" ?>)
   *
   * @param versionInfo the version info */
  @Override
  public void addXMLDecl(String versionInfo)
  {
    addXMLDecl(_xml, versionInfo);
  }

  /**
   * Adds the XML declaration (<?xml version="1.0" ?>)
   *
   * @param xml which buffer to append to
   * @param versionInfo the version info */
  static void addXMLDecl(StringBuilder xml, String versionInfo)
  {
    xml.append("<?xml version=\"").append(versionInfo).append("\"?>\n");
  }

  /**
   * Adds the XML declaration (<?xml version="1.0" encoding="UTF-8"
   * standalone="yes"?>)
   *
   * @param versionInfo the version info
   * @param encoding the encoding (eg: UTF-8)
   * @param standalone true for standalone */
  @Override
  public void addXMLDecl(String versionInfo,
                         String encoding,
                         boolean standalone)
  {
    _xml.append("<?xml version=\"").append(versionInfo);
    _xml.append("\" encoding=\"");
    _xml.append(encoding).append("\" standalone=\"");
    _xml.append(standalone ? "yes" : "no");
    _xml.append("\"?>\n");
  }

  /**
   * Adds an opening tag
   *
   * @param tagName the name of the opening tag to add */
  @Override
  public void addOpeningTag(String tagName)
  {
    _xml.append('<').append(tagName).append('>');
  }

  /**
   * Adds a closing tag
   *
   * @param tagName the name of the closing tag to add */
  @Override
  public void addClosingTag(String tagName)
  {
    _xml.append("</").append(tagName).append('>');
  }

  /**
   * Adds an empty tag
   *
   * @param tagName the name of the empty tag to add */
  @Override
  public void addEmptyTag(String tagName)
  {
    _xml.append('<').append(tagName).append(" />");
  }

  /**
   * Adds a tag
   *
   * @param tagName the name of the tag to add
   * @param tagValue the value of the tag to add */
  @Override
  public void addTag(String tagName, String tagValue)
  {
    if(tagValue == null)
      addEmptyTag(tagName);
    else
    {
      addOpeningTag(tagName);
      _xml.append(XMLUtils.xmlEncode(tagValue));
      addClosingTag(tagName);
    }
  }

  /**
   * Adds the given block of XML. If it is not a properly formatter block
   * of XML then the output will not be properly formatted.
   *
   * @param xml the block of XML to add */
  @Override
  public void addXML(String xml)
  {
    _xml.append(xml);
  }

  /**
   * Returns the xml generated */
  @Override
  public String getXML()
  {
    return _xml.toString();
  }

  /**
   * Call this method when you want to reset the internal string to start
   * from scratch again */
  @Override
  public void reset()
  {
    _xml.setLength(0);
  }

  /**
   * Adds an empty tag which contains the attributes specified. All the
   * properties will be turned into attributes.
   *
   * @param tagName the name of the empty tag to add
   * @param attributes the attributes  */
  @Override
  public void addEmptyTag(String tagName, Map attributes)
  {
    _xml.append('<').append(tagName);
    addAttributes(attributes);
    _xml.append(" />");
  }

  /**
   * Adds an empty tag which contains an attribute
   *
   * @param tagName the name of the empty tag to add
   * @param attrName the name of the attribute
   * @param attrValue the value of the attribute  */
  @Override
  public void addEmptyTag(String tagName, String attrName, String attrValue)
  {
    _xml.append('<').append(tagName);
    addAttribute(attrName, attrValue);
    _xml.append(" />");
  }

  /**
   * Adds an opening tag which contains the attributes specified. All the
   * properties will be turned into attributes.
   *
   * @param tagName the name of the opening tag to add
   * @param attributes the attributes  */
  @Override
  public void addOpeningTag(String tagName, Map attributes)
  {
   _xml.append('<').append(tagName);
   addAttributes(attributes);
   _xml.append('>');
  }

  /**
   * Adds an opening tag which contains an attribute
   *
   * @param tagName the name of the opening tag to add
   * @param attrName the name of the attribute
   * @param attrValue the value of the attribute  */
  @Override
  public void addOpeningTag(String tagName, String attrName, String attrValue)
  {
    _xml.append('<').append(tagName);
    addAttribute(attrName, attrValue);
    _xml.append('>');
  }

  /**
   * Adds a tag which contains the attributes specified. All the
   * properties will be turned into attributes.
   *
   * @param tagName the name of the tag to add
   * @param tagValue the value of the tag to add
   * @param attributes the attributes  */
  @Override
  public void addTag(String tagName, String tagValue, Map attributes)
  {
    if(tagValue == null)
      addEmptyTag(tagName, attributes);
    else
    {
      addOpeningTag(tagName, attributes);
      _xml.append(XMLUtils.xmlEncode(tagValue));
      addClosingTag(tagName);
    }
  }

  /**
   * Adds a tag which contains an attribute
   *
   * @param tagName the name of the tag to add
   * @param tagValue the value of the tag to add
   * @param attrName the name of the attribute
   * @param attrValue the value of the attribute  */
  @Override
  public void addTag(String tagName,
                     String tagValue,
                     String attrName,
                     String attrValue)
  {
    if(tagValue == null)
      addEmptyTag(tagName, attrName, attrValue);
    else
    {
      addOpeningTag(tagName, attrName, attrValue);
      _xml.append(XMLUtils.xmlEncode(tagValue));
      addClosingTag(tagName);
    }
  }

  /**
   * Adds the attribute
   *
   * @param attrName name of the attribute to add
   * @param attrValue value of the attribute to add */
  private void addAttribute(String attrName, String attrValue)
  {
    if(attrName == null)
      return;

    _xml.append(' ');
    _xml.append(attrName).append("=\"");
    _xml.append(XMLUtils.xmlEncode(attrValue)).append('"');
  }

  /**
   * Adds the attributes. All the properties will be used as attributes
   *
   * @param attributes the attributes to add */
  private void addAttributes(Map attributes)
  {
    if(attributes == null)
      return;

    Iterator iter = attributes.keySet().iterator();
    while(iter.hasNext())
    {
      String attrName = (String) iter.next();
      Object attrValue = attributes.get(attrName);
      if(attrValue != null)
        addAttribute(attrName, attrValue.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.resource;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;

import org.linkedin.util.clock.Timespan;
import org.linkedin.util.io.PathUtils;
import org.linkedin.util.io.resource.internal.URLResourceProvider;
import org.linkedin.util.url.URLBuilder;
import org.linkedin.util.io.resource.internal.AbstractResource;
import org.linkedin.util.io.resource.internal.InternalResourceProvider;

/**
 * @author ypujante@linkedin.com
 *
 */
public class URLResource extends AbstractResource
{
  private final URL _url;
  private final Timespan _connectTimeout;
  private final Timespan _readTimeout;

  public static final Timespan DEFAULT_CONNECT_TIMEOUT = Timespan.ZERO_MILLISECONDS;
  public static final Timespan DEFAULT_READ_TIMEOUT    = Timespan.ZERO_MILLISECONDS;

  /**
   * Constructor
   */
  protected URLResource(InternalResourceProvider resourceProvider, String path, URL url)
  {
    // 0's are the default values for these respective timeouts in URLConnection
    // see http://java.sun.com/j2se/1.5.0/docs/api/java/net/URLConnection.html
    this(resourceProvider, path, url, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT);
  }

  /**
   * @see java.net.URLConnection for an explanation of what the timeout values are all about.
   *
   * @param resourceProvider
   * @param path
   * @param url
   * @param connectTimeout
   * @param readTimeout
   */
  public URLResource(InternalResourceProvider resourceProvider, String path, URL url, Timespan connectTimeout, Timespan readTimeout)
  {
    super(resourceProvider, path);
    _url = url;

    _connectTimeout = connectTimeout;
    _readTimeout = readTimeout;
  }

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

  /**
   * 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
  {
    throw new IOException("not supported");
  }

  /**
   * 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 extractInfo(_url);
  }

  /**
   * 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
  {
    URLConnection urlConnection = _url.openConnection();

    urlConnection.setDoInput(true);
    urlConnection.setDoOutput(false);
    urlConnection.setUseCaches(false);
    urlConnection.setConnectTimeout(new Long(_connectTimeout.getDurationInMilliseconds()).intValue());
    urlConnection.setReadTimeout(new Long(_readTimeout.getDurationInMilliseconds()).intValue());
    urlConnection.connect();

    return urlConnection.getInputStream();
  }

  /**
   * @return true if the resource exists.
   */
  @Override
  public boolean exists()
  {
    try
    {
      InputStream is = getInputStream();
      try
      {
        return true;
      }
      finally
      {
        is.close();
      }
    }
    catch(IOException e)
    {
      return false;
    }
  }

  /**
   * @return a uri representation of the resource
   */
  @Override
  public URI toURI()
  {
    try
    {
      return _url.toURI();
    }
    catch(URISyntaxException e)
    {
      throw new RuntimeException(e);
    }
  }

  /**
   * Extracts the info from the url
   */
  public static ResourceInfo extractInfo(URL url) throws IOException
  {
    URLConnection urlConnection = url.openConnection();

    urlConnection.setDoInput(true);
    urlConnection.setDoOutput(false);
    urlConnection.setUseCaches(false);
    urlConnection.connect();
    InputStream is = urlConnection.getInputStream();
    try
    {
      return new StaticInfo(urlConnection.getContentLength(), urlConnection.getLastModified());
    }
    finally
    {
      is.close();
    }
  }

  /**
   * Creates the resource from a url...
   *
   * @param url
   * @return the resource
   */
  public static Resource create(URL url)
  {
    URLBuilder rootURL = URLBuilder.createFromPath("/");
    rootURL.setScheme(url.getProtocol());
    rootURL.setHost(url.getHost());
    rootURL.setPort(url.getPort());
    rootURL.setUserInfo(url.getUserInfo());

    return create(rootURL.toJavaURL(), "/").createRelative(url.getPath());
  }

  /**
   * Creates the resource from a url... The root will be at the path of this url (ex:
   * if url is http://localhost/a/b/c/foo.html then root is /a/b/c).
   *
   * @param url
   * @return the resource
   */
  public static Resource createFromRoot(URL url)
  {
    String urlPath = url.getPath();
    String path = "/";

    if(!urlPath.endsWith("/"))
    {
      urlPath = PathUtils.addLeadingSlash(urlPath);
      int idx = urlPath.lastIndexOf("/"); // there will always be one due to previous line...
      path = urlPath.substring(idx + 1);
      urlPath = urlPath.substring(0, idx);
    }

    URLBuilder rootURL = URLBuilder.createFromPath(urlPath);
    rootURL.setScheme(url.getProtocol());
    rootURL.setHost(url.getHost());
    rootURL.setPort(url.getPort());
    rootURL.setUserInfo(url.getUserInfo());

    return create(rootURL.toJavaURL(), "/").createRelative(path);
  }

  /**
   * Creates the resource from a url...
   *
   * @param url
   * @return the resource
   */
  public static Resource create(URL url, String rootPath)
  {
    try
    {
      return new URLResourceProvider(url, rootPath).getRootResource();
    }
    catch(UnsupportedURIException e)
    {
      throw new RuntimeException(e);
    }
    catch(URISyntaxException 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.resource.Resource;
import org.linkedin.util.io.resource.URIResourceFactory;
import org.linkedin.util.io.resource.URLResource;

import java.net.MalformedURLException;
import java.net.URI;

/**
 * Handles generic URL.. the scheme is provided. (ex: http, ftp...)
 *
 * @author ypujante@linkedin.com
 *
 */
public class SchemeURLResourceFactory extends SchemeURIResourceFactory
{
  private final String _scheme;

  /**
   * Constructor
   */
  public SchemeURLResourceFactory(String scheme)
  {
    _scheme = scheme;
  }

  /**
   * @return the scheme that this factory handles
   */
  @Override
  public String getScheme()
  {
    return _scheme;
  }

  /**
   * Method that needs to be implemented by children..
   *
   * @param uri
   * @param parent the parent resource factory
   * @return the resource
   */
  @Override
  protected Resource doCreateResource(URI uri, URIResourceFactory parent)
  {
    try
    {
      return URLResource.createFromRoot(uri.toURL());
    }
    catch(MalformedURLException e)
    {
      throw new IllegalArgumentException(uri.toString(), 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.resource.Resource;
import org.linkedin.util.io.resource.ResourceFilter;

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

/**
 * @author ypujante@linkedin.com
 *
 */
public abstract class AbstractResourceProvider implements InternalResourceProvider
{
  /**
   * Constructor
   */
  public AbstractResourceProvider()
  {
  }

  /**
   * The path to the resource is absolute. Whether it starts with / or not it will be made
   * absolute.
   *

   * Note that it follows the same convention as {@link File} class in the sense that a resource
   * simply represents a handle and does not imply that the resource exists.
   *
   * @param path the path to the resource
   * @return the resource given the path
   */
  @Override
  public Resource createResource(String path)
  {
    if(path == null)
      return null;

    URI uri = URI.create(path).normalize();

    if(uri.isAbsolute())
      throw new IllegalArgumentException(path + " => only path with no scheme are supported...");

    path = uri.toString();

    return doBuildResource(PathUtils.addLeadingSlash(path));
  }

  /**
   * @param resource
   * @return the parent of a resource
   */
  @Override
  public InternalResource getParentResource(InternalResource resource)
  {
    String parentPath = PathUtils.getParentPath(resource.getPath());

    if(parentPath.equals(resource.getPath()))
      // we are at the root...
      return resource;
    else
      return doBuildResource(parentPath);
  }

  /**
   * @return the root resource
   */
  @Override
  public InternalResource getRootResource()
  {
    return doBuildResource("/");
  }

  /**
   * @param resource
   * @param relativePath the relative path
   * @return a resource relative to the resource
   */
  @Override
  public final InternalResource createRelative(InternalResource resource, String relativePath)
  {
    if(relativePath == null)
      return null;

    URI relativeURI = URI.create(relativePath).normalize(); // normalize removes leading .

    if(relativeURI.isAbsolute())
      throw new IllegalArgumentException(relativePath + " is absolute");

    relativePath = relativeURI.getPath();

    if(!"/".equals(relativePath))
      // we remove the leading / only when it is not the only thing
      relativePath = PathUtils.removeLeadingSlash(relativePath);

    if("".equals(relativePath))
      return resource;

    if("..".equals(relativePath))
      return getParentResource(resource);

    if(relativePath.startsWith("../"))
      return (InternalResource) getParentResource(resource).createRelative(relativePath.substring(3));

    return doBuildResource(PathUtils.addPaths(resource.getPath(), relativePath));
  }

  /**
   * Returns a new resource with the root resource set to this resource.
   *
   * @return the new resource
   */
  @Override
  public InternalResource chroot(InternalResource resource)
  {
    String path = resource.getPath();

    // when it is a directory, we simply use the provided resource
    if(resource.isDirectory())
    {
      return doCreateResourceProvider(path).getRootResource();
    }
    else
    {
      // when not a directory, we do a chroot to the parent and then we return the resource
      // that points to it
      return (InternalResource) resource.chroot("..").createRelative(resource.getFilename());
    }
  }

  /**
   * If this resource denotes a directory, then it will return all resources that are contained in
   * the directory.
   *
   * @param filter the filter to include only some resources in the result
   * @return all the resources contained in the directory or null if not a directory
   * @throws IOException if there is an error accessing the resource
   */
  @Override
  public InternalResource[] list(InternalResource resource, final ResourceFilter filter)
    throws IOException
  {
    final List<Resource> resources = new ArrayList<Resource>();

    String path = PathUtils.addTrailingSlash(resource.getPath());

    if(doList(path, new ResourceFilter()
    {
      @Override
      public boolean accept(Resource resource)
      {
        boolean res = filter.accept(resource);
        if(res)
          resources.add(resource);
        return res;
      }
    }))
    {
      return resources.toArray(new InternalResource[resources.size()]);
    }
    else
      return null;
  }
}


/*
 * 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.resource.AcceptAllResourceFilter;
import org.linkedin.util.io.resource.Resource;
import org.linkedin.util.io.resource.ResourceFilter;

import java.io.IOException;

/**
 * Base implementation of all resources. Implement the basic behavior and will delegate to
 * subclasses for actual implementation (strategy pattern).
 *
 * @author ypujante@linkedin.com
 *
 */
public abstract class AbstractResource implements InternalResource
{
  private final InternalResourceProvider _resourceProvider;
  private final String _path;

  /**
   * Constructor
   */
  protected AbstractResource(InternalResourceProvider resourceProvider, String path)
  {
    if(!path.startsWith("/"))
      throw new IllegalArgumentException(path + " must start with /");
    if(resourceProvider == null)
      throw new NullPointerException("resource provider cannot be null");

    _resourceProvider = resourceProvider;
    _path = path;
  }

  /**
   * The path always starts with '/'.
   *
   * @return the path to the resource (within the context of the resource provider)
   */
  @Override
  public String getPath()
  {
    return _path;
  }

  /**
   * @return the resource provider that created this resource
   */
  @Override
  public ResourceProvider getResourceProvider()
  {
    return _resourceProvider;
  }

  /**
   * If this resource denotes a directory, then it will return all resources that are contained in
   * the directory.
   *
   * @return all the resources contained in the directory or null if not a directory
   * @throws IOException if there is an error accessing the resource
   */
  @Override
  public final Resource[] list() throws IOException
  {
    return list(AcceptAllResourceFilter.INSTANCE);
  }

  /**
   * Creates a resource relative to the current one. Leading / is optional. It is technically
   * equivalent to: getRootResource().createRelative(getPath() + "/" + relativePath).
   * The following properties apply:
   *
   *

   *

   * createRelative("abc") is the same as
   * createRelative("/abc")
   *

   *

   * createRelative("/") will add a trailing / to the path if there isn't already
   * one
   *

   *

   * createRelative("") == createRelative(".") == createRelative("./") == this
   *

   *

   * createRelative("..") is the same as getParentResource()
   *

   *

   * if this resource == root resource then createRelative("..") is the same as
   * createRelative(".") (you cannot go higher than the root...)
   *

   *

   * createRelative("../abc") is the same as
   * getParentResource().createRelative("abc")
   *

   *

   *
   * @param relativePath the relative path
   * @return the new resource
   */
  @Override
  public Resource createRelative(String relativePath)
  {
    return _resourceProvider.createRelative(this, relativePath);
  }

  /**
   * Returns a new resource with the root resource set to the relative path provided. Note that the
   * new resource points at the new root as well.
   *
   * @param relativePath
   * @return the new resource
   */
  @Override
  public Resource chroot(String relativePath)
  {
    return _resourceProvider.chroot((InternalResource) createRelative(relativePath));
  }

  /**
   * @return the parent resource. Same meaning as File.getParentFile().
   */
  @Override
  public Resource getParentResource()
  {
    return _resourceProvider.getParentResource(this);
  }

  /**
   * @return the root resource
   * @see #createRelative(String)
   */
  @Override
  public Resource getRootResource()
  {
    return _resourceProvider.getRootResource();
  }

  /**
   * If this resource denotes a directory, then it will return all resources that are contained in
   * the directory.
   *
   * @param filter the filter to include only some resources in the result
   * @return all the resources contained in the directory or null if not a directory
   * @throws IOException if there is an error accessing the resource
   */
  @Override
  public Resource[] list(ResourceFilter filter) throws IOException
  {
    return _resourceProvider.list(this, filter);
  }

  /**
   * Shortcut to getInfo().getLastModified() with the same meaning as the
   * File.lastModified api: returns 0L if the resource does not exist or
   * if an IO error occurs.
   *
   * @return the last modified date of this resource.
   */
  @Override
  public long lastModified()
  {
    if(!exists())
      return 0L;

    try
    {
      return getInfo().getLastModified();
    }
    catch(IOException e)
    {
      return 0L;
    }
  }

  /**
   * 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 lastModified() > time;
  }

  /**
   * Shortcut to getInfo().getContentLength() with the same meaning as the
   * File.length() api: returns 0L if the resource does not exist or if an
   * IO error occurs.
   *
   * @return the length in bytes of the resource.
   */
  @Override
  public long length()
  {
    if(!exists())
      return 0L;

    try
    {
      return getInfo().getContentLength();
    }
    catch(IOException e)
    {
      return 0L;
    }
  }

  /**
   * @return the filename portion of this resource. If it is a directory, returns the name of the
   *         directory.
   */
  @Override
  public String getFilename()
  {
    String path = PathUtils.removeTrailingSlash(_path);

    int idx = path.lastIndexOf("/");
    if(idx >= 0)
      path = path.substring(idx + 1);
    return path;
  }

  /**
   * Returns a string representation of the object. In general, the toString method
   * returns a string that "textually represents" this object. The result should be a concise but
   * informative representation that is easy for a person to read. It is recommended that all
   * subclasses override this method.
   *

   * The toString method for class Object returns a string consisting of
   * the name of the class of which the object is an instance, the at-sign character
   * `@', and the unsigned hexadecimal representation of the hash code of the object.
   * In other words, this method returns a string equal to the value of:

   *

   * getClass().getName() + '@' + Integer.toHexString(hashCode())
   *

   *
   * @return a string representation of the object.
   */
  @Override
  public String toString()
  {
    return toURI().toString();
  }

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

    AbstractResource that = (AbstractResource) o;

    if(!_path.equals(that._path)) return false;
    if(!_resourceProvider.equals(that._resourceProvider)) return false;

    return true;
  }

  @Override
  public int hashCode()
  {
    int result;
    result = _resourceProvider.hashCode();
    result = 31 * result + _path.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.exceptions;

import java.lang.reflect.InvocationTargetException;

/**
 * Exception thrown when there is an internal problem. Should *not* be used for business style
 * exception (bad example: NoSuchMemberException). Usually used to wrap other low
 * level exceptions (ex: a SQLException reprensenting the fact that the db is down).
 *
 *
 * @author ypujante@linkedin.com */
public class InternalException extends RuntimeException
{
  private static final long serialVersionUID = 1L;

  public InternalException(String module, Throwable detail)
  {
    super(module, detail);
  }

  public InternalException(String module, String detail)
  {
    super(module + ":" + detail);
  }

  public InternalException(String module, String detail, Throwable th)
  {
    super(module + ":" + detail, th);
  }

  public InternalException(String module)
  {
    super(module);
  }

  public InternalException(Throwable th)
  {
    super(th);
  }

  public InternalException()
  {
    super();
  }

  /**
   * Convenient call to adapt the InvocationTargetException into an
   * InternalException
   *
   * @param module the module
   * @param ite the invocation target exception (coming from Method.invoke)
   * @throws InternalException this method always throw an exception at the end: either a
   * RuntimeException or Error or InternalException
   */
  public static InternalException throwInternalException(String module,
                                                         InvocationTargetException ite)
    throws InternalException
  {
    try
    {
      throw ite.getTargetException();
    }
    catch(Error e)
    {
      throw e;
    }
    catch(RuntimeException e)
    {
      throw e;
    }
    catch(Throwable e)
    {
      throw new InternalException(module, 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.xml;

/**
 * Contains XML related utilities
 *
 * @author ypujante@linkedin.com */
public class XMLUtils
{
  private final static String[] _chars;

  static
  {
    // we create a static array that will convert all the characters from 0 to 127 into their xml
    // representation. Note that the characters from [0-20[ are not valid except tab, CR and
    // linefeed. So we replace them with <space>.
    _chars = new String[128];

    for(int i = ' '; i < _chars.length; i++)
     _chars[i] = String.valueOf((char) i);

    for(int i = 0; i < ' '; i++)
      _chars[i] = " ";

    // valid xml characters
    _chars['\t'] = "\t";
    _chars['\n'] = "\n";
    _chars['\r'] = "\r";

    // the ones who need encoding
    _chars['<'] = "<";
    _chars['>'] = ">";
    _chars['&'] = "&";
    _chars['"'] = """;
  }

  /**
   * Encodes the string so that it is XML safe: converts <, > &
   * ' into their equivalent (&lt;, &gt; &amp;
   * &apos;)
   *
   * @param original the string to encode
   * @return the encoded string */
  public static String xmlEncode(String original)
  {
    if(original == null)
      return null;

    char[] chars = original.toCharArray();
    final int len = chars.length;
    StringBuilder sb = new StringBuilder(len);

    for(int i = 0; i < chars.length; i++)
    {
      char c = chars[i];
      if(c < _chars.length)
      {
        sb.append(_chars[c]);
      }
      else if (Character.isHighSurrogate(c))
      {
        int j = i+1;
        if (j < len && Character.isLowSurrogate(chars[j]))
        {
          // use character reference anyway on this character, since the handling
          // of some big code blocks might not be well defined in certain encodings.
          sb.append("&#x").append(Integer.toHexString(Character.toCodePoint(c, chars[j]))).append(';');
          i = j;
        }
        // else ignore this character, a single high surrogate is an invalid char
      }
      else if (!Character.isLowSurrogate(c))
      {
        sb.append(c);
      }
    }

    return sb.toString();
  }

  /**
   * Encodes the string so that it is XML safe: all characters <= 127
   * will not be encoded (converts <, > & ' into their
   * equivalent (&lt;, &gt; &amp; &apos;)). All characters
   * > 127 will be encoded as &#xxxx; with xxxx the hexadecimal
   * value of the character. This method differs from
   * xmlEncode because it encodes all values > 127 into a
   * pure ascii xml safe string. In most cases, this method is NOT
   * necessary because with an xml message you can specify the encoding, so
   * there is no need to encode those characters.
   *
   * @param s the string to encode
   * @return the encoded string */
  public static String xmlEncodeRaw(String s)
  {
    if(s == null)
      return null;

    char[] chars = s.toCharArray();
    final int len = chars.length;
    StringBuilder sb = new StringBuilder(len);

    for(int i = 0; i < len; i++)
    {
      char c = chars[i];
      if(c < _chars.length)
      {
        sb.append(_chars[c]);
      }
      else if (Character.isHighSurrogate(c))
      {
        int j = i+1;
        if (j < len && Character.isLowSurrogate(chars[j]))
        {
          sb.append("&#x").append(Integer.toHexString(Character.toCodePoint(c, chars[j]))).append(';');
          i = j;
        }
        // else ignore this character, a single high surrogate is an invalid char
      }
      else if (!Character.isLowSurrogate(c))
      {
        sb.append("&#x").append(Integer.toHexString(c)).append(';');
      }
    }

    return sb.toString();
  }

  /**
   * Encapsulates what to do for html. Delegate to {@link #xmlEncodeRaw(String)}.
   *
   * @param s
   * @return  the htmlized string
   * @see #xmlEncodeRaw(String) */
  public static String htmlEncode(String s)
  {
    return xmlEncodeRaw(s);
  }

  /**
   * This method descodes a string that was previously encoded for being XML
   * safe. It is the exact opposite of xmlEncode
   *
   * @param s the string to decode
   * @return the decoded string
   * @exception IllegalArgumentException if the string cannot be decoded */
  public static String xmlDecode(String s) throws IllegalArgumentException
  {
    if(s == null)
      return s;

    int idxS = s.indexOf('&');
    if(idxS < 0)
      return s;

    StringBuilder sb = new StringBuilder(s.length());

    int idxE, idx, size;
    char c, c_1;
    int prev = 0;

    while(idxS != -1)
    {
      idxE = s.indexOf(';', idxS);
      if(idxE < 0)
        throw new IllegalArgumentException("missing ';' in: " +
                                           s.substring(idxS));

      sb.append(s.substring(prev, idxS));

      idx = idxS + 1;
      size = idxE - idxS - 1;
      if(size < 2)
        throw new IllegalArgumentException("invalid escape tag: " +
                                           s.substring(idxS, idxE + 1));
      c = s.charAt(idx);
      c_1 = s.charAt(idx + 1);
      switch(c)
      {
        case 'l':
          if(size != 2)
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          if(c_1 == 't')
            sb.append('<');
          else
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          break;

        case 'g':
          if(size != 2)
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          if(c_1 == 't')
            sb.append('>');
          else
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          break;

        case 'q':
          if(size != 4)
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          if(c_1 != 'u' || s.charAt(idx + 2) != 'o' ||
             s.charAt(idx + 3) != 't')
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          else
            sb.append('"');
          break;

        case 'a':
          if(size == 3)
          {
            if(c_1 != 'm' || s.charAt(idx + 2) != 'p')
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
            else
              sb.append('&');
          }
          else if(size == 4)
          {
            if(c_1 != 'p' || s.charAt(idx + 2) != 'o' ||
               s.charAt(idx + 3) != 's')
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
            else
              sb.append('\'');
          }
          else
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          break;

        case '#':
          int codePoint;
          try
          {
            codePoint = (c_1 == 'x')
              ? Integer.parseInt(s.substring(idx + 2, idxE), 16)
              : Integer.parseInt(s.substring(idx + 1, idxE));
          }
          catch(NumberFormatException ex)
          {
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
          }

          if (codePoint < 0)
            throw new IllegalArgumentException("invalid character codepoint: " +
                                               s.substring(idxS, idxE + 1));

          if (codePoint < Character.MIN_SUPPLEMENTARY_CODE_POINT)
            sb.append((char) codePoint);
          else
            sb.append(Character.toChars(codePoint));
          break;

        default:
            throw new IllegalArgumentException("invalid escape tag: " +
                                               s.substring(idxS, idxE + 1));
      }
      prev = idxE + 1;
      idxS = s.indexOf("&", prev);
    }
    if(prev < s.length())
      sb.append(s.substring(prev));

    return sb.toString();
  }

  public static String xmlDecodeSafely(String str)
  {
    // The following code is a copy of XMLUtils.xmlDecode() but without any IllegalArgumentExceptions thrown on unidentified or malformed entities.
    // We need this ability because our input may or may not have properly escaped entities, and we should be able to handle both cases gracefully.
    // It also replaces   occurrences with a space.
    // In casual perf tests this method performs better (~4x faster) than a corresponding method that uses a series of String.replace() calls.
    int idxS = str.indexOf("&");
    if(idxS < 0)
    {
      // No entities to decode, so return String as is.
      return str;
    }

    StringBuilder sb = new StringBuilder(str.length());
    int idxE, idx, size;
    char c;
    int prev = 0;

    while(idxS != -1)
    {
      if (prev < idxS)
      {
        sb.append(str.substring(prev, idxS));
        // Update prev variable to indicate what's been consumed so far.
        prev = idxS;
      }

      idxE = str.indexOf(";", idxS);
      if(idxE < 0)
      {
        // No more properly formed entities to decode from this point on.
        break;
      }

      idx = idxS + 1;
      size = idxE - idxS - 1;
      if(size < 2)
      {
        // Unknown entity, so just append the faux entity as is and move on.
        sb.append(str.substring(idxS, idxE + 1));
      }
      else
      {
        c = str.charAt(idx);
        switch(c)
        {
          case 'l':
            if (!xmlDecodeLT(str, idx, sb, size))
            {
              // Unknown entity, so just consume the entity start (i.e. the ampersand) and move on.
              sb.append("&");
              idxE = idxS;
            }
            break;

          case 'g':
            if (!xmlDecodeGT(str, idx, sb, size))
            {
              // Unknown entity, so just consume the entity start (i.e. the ampersand) and move on.
              sb.append("&");
              idxE = idxS;
            }
            break;

          case 'q':
            if (!xmlDecodeQUOT(str, idx, sb, size))
            {
              // Unknown entity, so just consume the entity start (i.e. the ampersand) and move on.
              sb.append("&");
              idxE = idxS;
            }
            break;

          case 'a':
            if (!xmlDecodeAMPAPOS(str, idx, sb, size))
            {
              // Unknown entity, so just consume the entity start (i.e. the ampersand) and move on.
              sb.append("&");
              idxE = idxS;
            }
            break;

          case 'n':
            if (!xmlDecodeNBSP(str, idx, sb, size))
            {
              // Unknown entity, so just consume the entity start (i.e. the ampersand) and move on.
              sb.append("&");
              idxE = idxS;
            }
            break;

          case '#':
            if (!xmlDecodeNumber(str, idx, sb, idxE))
            {
              // Unknown entity, so just consume the entity start (i.e. the ampersand) and move on.
              sb.append("&");
              idxE = idxS;
            }
            break;

          default:
            // Unknown entity, so just consume the entity start (i.e. the ampersand) and move on.
            sb.append("&");
            idxE = idxS;
        }
      }

      prev = idxE + 1;
      idxS = str.indexOf("&", prev);
    }

    if (prev < str.length())
    {
      sb.append(str.substring(prev));
    }

    return sb.toString();
  }

  private static boolean xmlDecodeLT(String str, int idx, StringBuilder sb, int size)
  {
    boolean isRecognized = true;
    char c_1 = str.charAt(idx + 1);

    if ((size != 2) || (c_1 != 't'))
    {
      isRecognized = false;
    }
    else
    {
      // lt
      sb.append('<');
    }

    return isRecognized;
  }

  private static boolean xmlDecodeGT(String str, int idx, StringBuilder sb, int size)
  {
    boolean isRecognized = true;
    char c_1 = str.charAt(idx + 1);
    if ((size != 2) || (c_1 != 't'))
    {
      isRecognized = false;
    }
    else
    {
      // gt
      sb.append('>');
    }

    return isRecognized;
  }

  private static boolean xmlDecodeQUOT(String str, int idx, StringBuilder sb, int size)
  {
    boolean isRecognized = true;
    char c_1 = str.charAt(idx + 1);
    if ((size != 4) || (c_1 != 'u' || str.charAt(idx + 2) != 'o' || str.charAt(idx + 3) != 't'))
    {
      isRecognized = false;
    }
    else
    {
      // quot
      sb.append('"');
    }

    return isRecognized;
  }

  private static boolean xmlDecodeAMPAPOS(String str, int idx, StringBuilder sb, int size)
  {
    boolean isRecognized = true;
    char c_1 = str.charAt(idx + 1);
    if ((size == 3) && (c_1 == 'm') && (str.charAt(idx + 2) == 'p'))
    {
      // amp
      sb.append('&');
    }
    else if ((size == 4) && (c_1 == 'p') && (str.charAt(idx + 2) == 'o') && (str.charAt(idx + 3) == 's'))
    {
      // apos
      sb.append('\'');
    }
    else
    {
      isRecognized = false;
    }

    return isRecognized;
  }

  private static boolean xmlDecodeNBSP(String str, int idx, StringBuilder sb, int size)
  {
    boolean isRecognized = true;
    char c_1 = str.charAt(idx + 1);

    if ((size != 4) || (c_1 != 'b' || str.charAt(idx + 2) != 's' || str.charAt(idx + 3) != 'p'))
    {
      isRecognized = false;
    }
    else
    {
      // nbsp
      sb.append(' ');
    }

    return isRecognized;
  }

  private static boolean xmlDecodeNumber(String str, int idx, StringBuilder sb, int idxE)
  {
    boolean isRecognized = true;

    try
    {
      char c = 0;
      char c_1 = str.charAt(idx + 1);
      if(c_1 == 'x')
      {
        c = (char) Integer.parseInt(str.substring(idx + 2, idxE), 16);
      }
      else
      {
        c = (char) Integer.parseInt(str.substring(idx + 1, idxE));
      }
      sb.append(c);
    }
    catch(NumberFormatException ex)
    {
      isRecognized = false;
    }

    return isRecognized;
  }

  /**
   * Constructor. Do not use */
  private XMLUtils()
  {}
}


/*
 * 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.resource.FilteredResource;
import org.linkedin.util.io.resource.Resource;
import org.linkedin.util.io.resource.ResourceFilter;

/**
 * @author ypujante@linkedin.com
 *
 */
public class FilteredResourceProvider extends AbstractResourceProvider
{
  private final InternalResourceProvider _resourceProvider;

  /**
   * Constructor
   */
  public FilteredResourceProvider(InternalResourceProvider resourceProvider)
  {
    _resourceProvider = resourceProvider;
  }

  /**
   * Constructor
   */
  public FilteredResourceProvider(Resource resource)
  {
    this((InternalResourceProvider) ((InternalResource) resource).getResourceProvider());
  }

  /**
   * @return the filtered resource provider
   */
  public InternalResourceProvider getFilteredResourceProvider()
  {
    return _resourceProvider;
  }

  /**
   * 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 doBuildResource(_resourceProvider.doBuildResource(path));
  }

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

  /**
   * This method will be implemented by subclasses to create the right kind of resource provider
   *
   * @param resourceProvider
   * @return the resource provider
   */
  protected InternalResourceProvider doCreateResourceProvider(InternalResourceProvider resourceProvider)
  {
    return new FilteredResourceProvider(resourceProvider);
  }

  /**
   * 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, final ResourceFilter filter)
  {
    ResourceFilter newFilter = new ResourceFilter()
    {
      @Override
      public boolean accept(Resource resource)
      {
        return filter.accept(doBuildResource(resource));
      }
    };

    return _resourceProvider.doList(path, newFilter);
  }

  /**
   * This method will be implemented by subclasses to create the right kind of resource.
   *
   * @param filteredResource
   * @return the resource
   */
  protected InternalResource doBuildResource(Resource filteredResource)
  {
    return new FilteredResource(this, filteredResource.getPath(), filteredResource);
  }

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

    FilteredResourceProvider that = (FilteredResourceProvider) o;

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

    return true;
  }

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


/*
 * 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.PathUtils;
import org.linkedin.util.io.resource.internal.AbstractResource;
import org.linkedin.util.io.resource.internal.FileResourceProvider;
import org.linkedin.util.io.resource.internal.InternalResourceProvider;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;

/**
 * @author ypujante@linkedin.com
 *
 */
// TODO MED YP:  toURI should preserve trailing slash...
public class FileResource extends AbstractResource
{
  private final File _file;

  /**
   * Basic implementation for file...
   */
  private class FileInfo implements ResourceInfo
  {
    @Override
    public long getContentLength() throws IOException
    {
      return _file.isDirectory() ? 0 : _file.length();
    }

    @Override
    public long getLastModified() throws IOException
    {
      return _file.lastModified();
    }
  }

  /**
   * Constructor
   */
  public FileResource(InternalResourceProvider resourceProvider, String path, File file)
  {
    super(resourceProvider, path);
    _file = file;
  }

  /**
   * @return true if the resource exists.
   */
  @Override
  public boolean exists()
  {
    return _file.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 _file;
  }

  /**
   * 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 new FileInputStream(_file);
  }

  /**
   * Efficiently returns all information about the resource.
   *
   * @return information about this resource.
   * @throws IOException if cannot get information
   */
  @Override
  public ResourceInfo getInfo() throws IOException
  {
    if(_file.exists())
      return new FileInfo();
    else
      throw new FileNotFoundException(_file.getPath());
  }

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

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

  /**
   * Creates a file resource from a file
   *
   * @param filename name of the file
   * @return the resource
   */
  public static Resource create(String filename)
  {
    return create(new File(filename));
  }

  /**
   * Creates a file resource from a file
   *
   * @param file the file
   * @return the resource (points to this file)
   */
  public static Resource create(File file)
  {
    try
    {
      String path = file.getCanonicalPath();
      if(file.isDirectory())
        path = PathUtils.addTrailingSlash(path);
      return create(new File("/"), path);
    }
    catch(IOException e)
    {
      throw new IllegalArgumentException("invalid file " + file, e);
    }
  }

  /**
   * Creates a file resource from a file, with the root as this file (if it is a directory
   * otherwise its parent).
   *
   * @param rootFile the root file
   * @return the resource (points to this file)
   */
  public static Resource createFromRoot(File rootFile)
  {
    File root = rootFile;
    String path = "/";

    if(!root.isDirectory())
    {
      root = rootFile.getParentFile();
      path = rootFile.getName();
    }

    return create(root, path);
  }

  /**
   * Creates a file resource with the root provided and the path (relative to the root).
   *
   * @param root the root of the resource
   * @param path the path (relative to root)
   * @return the resource
   */
  public static Resource create(File root, String path)
  {
    FileResourceProvider frp = null;
    try
    {
      frp = new FileResourceProvider(root);
    }
    catch(IOException e)
    {
      throw new RuntimeException(e);
    }
    return frp.createResource(path);
  }
}


/*
 * 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.linkedin.util.clock.Chronos;

import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.io.BufferedInputStream;
import java.io.UnsupportedEncodingException;
import java.io.File;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.TimeoutException;

/**
 * This class encapsulates a java Process to handle properly
 * output and error and preventing potential deadlocks. The idea is that the
 * command is executed and you can get the error or output. Simple to use.
 * Should not be used for big outputs because they are buffered internally.
 *
 * @author ypujante@linkedin.com
 */
public class ExternalCommand
{
  public static final String MODULE = ExternalCommand.class.getName();
  public static final Logger log = LoggerFactory.getLogger(MODULE);

  private final ProcessBuilder _processBuilder;

  private Process _process;
  private InputReader _out;
  private InputReader _err;

  private static class InputReader extends Thread
  {
    private final static int BUFFER_SIZE = 2048;

    private final InputStream _in;
    private final ByteArrayOutputStream _out;
    private boolean _running = false;

    InputReader(InputStream in)
    {
      _in = in;
      _out = new ByteArrayOutputStream();
    }

    @Override
    public void run()
    {
      _running = true;

      byte[] buf = new byte[BUFFER_SIZE];
      int n = 0;
      try
      {
        while((n = _in.read(buf)) != -1)
          _out.write(buf, 0, n);
      }
      catch(IOException e)
      {
        log.error("error while reading external command", e);
      }

      _running = false;
    }

    public byte[] getOutput()
    {
      if(_running)
        throw new IllegalStateException("wait for process to be completed");

      return _out.toByteArray();
    }
  }
  /**
   * Constructor */
  public ExternalCommand(ProcessBuilder processBuilder)
  {
    _processBuilder = processBuilder;
  }

  /**
   * After creating the command, you have to start it...
   *
   * @throws IOException
   */
  public void start() throws IOException
  {
    _process = _processBuilder.start();
    _out = new InputReader(new BufferedInputStream(_process.getInputStream()));
    _err = new InputReader(new BufferedInputStream(_process.getErrorStream()));

    _out.start();
    _err.start();
  }

  /**
   * @see ProcessBuilder
   */
  public Map<String, String> getEnvironment()
  {
    return _processBuilder.environment();
  }

  /**
   * @see ProcessBuilder
   */
  public File getWorkingDirectory()
  {
    return _processBuilder.directory();
  }

  /**
   * @see ProcessBuilder
   */
  public void setWorkingDirectory(File directory)
  {
    _processBuilder.directory(directory);
  }

  /**
   * @see ProcessBuilder
   */
  public boolean getRedirectErrorStream()
  {
    return _processBuilder.redirectErrorStream();
  }

  /**
   * @see ProcessBuilder
   */
  public void setRedirectErrorStream(boolean redirectErrorStream)
  {
    _processBuilder.redirectErrorStream(redirectErrorStream);
  }

  public byte[] getOutput() throws InterruptedException
  {
    waitFor();
    return _out.getOutput();
  }

  public byte[] getError() throws InterruptedException
  {
    waitFor();
    return _err.getOutput();
  }

  /**
   * Returns the output as a string.
   *
   * @param encoding
   * @return output with encoding
   * @throws InterruptedException
   * @throws UnsupportedEncodingException
   */
  public String getStringOutput(String encoding) throws InterruptedException,
                                                        UnsupportedEncodingException
  {
    return new String(getOutput(), encoding);
  }

  /**
   * Returns the output as a string. Uses encoding "UTF-8".
   *
   * @return output
   * @throws InterruptedException
   */
  public String getStringOutput() throws InterruptedException
  {
    try
    {
      return getStringOutput("UTF-8");
    }
    catch(UnsupportedEncodingException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns the error as a string.
   *
   * @param encoding
   * @return string representing the error with given encoding
   * @throws InterruptedException
   * @throws UnsupportedEncodingException
   */
  public String getStringError(String encoding) throws InterruptedException,
                                                       UnsupportedEncodingException
  {
    return new String(getError(), encoding);
  }

  /**
   * Returns the error as a string. Uses encoding "UTF-8".
   *
   * @return string representing the error
   * @throws InterruptedException
   */
  public String getStringError() throws InterruptedException
  {
    try
    {
      return getStringError("UTF-8");
    }
    catch(UnsupportedEncodingException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }
  }

  /**
   * Properly waits until everything is complete: joins on the thread that
   * reads the output, joins on the thread that reads the error and finally
   * wait for the process to be finished.
   * @return the status code of the process.
   *
   * @throws InterruptedException
   */
  public int waitFor() throws InterruptedException
  {
    if(_process == null)
      throw new IllegalStateException("you must call start first");

    _out.join();
    _err.join();
    return _process.waitFor();
  }

  /**
   * Properly waits until everything is complete: joins on the thread that
   * reads the output, joins on the thread that reads the error and finally
   * wait for the process to be finished.
   * If the process has not completed before the timeout, throws a
   * {@link TimeoutException}
   * @return the status code of the process.
   *
   * @throws TimeoutException
   * @throws InterruptedException
   */
  public int waitFor(long timeout) throws InterruptedException, TimeoutException
  {
    if(_process == null)
      throw new IllegalStateException("you must call start first");

    Chronos c = new Chronos();
    _out.join(timeout);
    timeout -= c.tick();
    if (timeout <= 0)
      throw new TimeoutException("Wait timed out");
    _err.join(timeout);
    timeout -= c.tick();
    if (timeout <= 0)
      throw new TimeoutException("Wait timed out");

    // there is no timeout in this API, not much we can do here
    // waiting on the other two threads should give us some safety
    return _process.waitFor();
  }

  public int exitValue()
  {
    if(_process == null)
      throw new IllegalStateException("you must call start first");

    return _process.exitValue();
  }

  public void destroy()
  {
    if(_process == null)
      throw new IllegalStateException("you must call start first");

    _process.destroy();
  }

  /**
   * Creates an external process from the command. It is not started and you have to call
   * start on it!
   *
   * @param commands the command to execute
   * @return the process */
  public static ExternalCommand create(String... commands)
  {
    ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands));
    return ec;
  }

  /**
   * Creates an external process from the command. It is not started and you have to call
   * start on it!
   *
   * @param commands the command to execute
   * @return the process */
  public static ExternalCommand create(List<String> commands)
  {
    ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands));
    return ec;
  }

  /**
   * Creates an external process from the command. The command is executed.
   *
   * @param commands the commands to execute
   * @return the process
   * @throws IOException if there is an error */
  public static ExternalCommand start(String... commands) throws IOException
  {
    ExternalCommand ec = new ExternalCommand(new ProcessBuilder(commands));
    ec.start();
    return ec;
  }

  /**
   * Executes the external command in the given working directory and waits for it to be
   * finished.
   *
   * @param workingDirectory the root directory from where to run the command
   * @param command the command to execute (should be relative to the working directory
   * @param args the arguments to the command
   * @return the process */
  public static ExternalCommand execute(File workingDirectory,
                                        String command,
                                        String... args)
      throws IOException, InterruptedException
  {
    try
    {
      return executeWithTimeout(workingDirectory, command, 0, args);
    }
    catch (TimeoutException e)
    {
      // Can't happen!
      throw new IllegalStateException(MODULE + ".execute: Unexpected timeout occurred!");
    }
  }

/**
 * Executes the external command in the given working directory and waits (until timeout
 * is elapsed) for it to be finished.
 *
 * @param workingDirectory
 *            the root directory from where to run the command
 * @param command
 *            the command to execute (should be relative to the working directory
 * @param timeout
 *            the maximum amount of time to wait for this external command (in ms). If
 *            this value is less than or equal to 0, timeout is ignored
 * @param args
 *            the arguments to the command
 * @return the process
 */
  public static ExternalCommand executeWithTimeout(File workingDirectory,
                                                   String command,
                                                   long timeout,
                                                   String... args)
      throws IOException, InterruptedException, TimeoutException
  {
    List<String> arguments = new ArrayList<String>(args.length + 1);

    arguments.add(new File(workingDirectory, command).getAbsolutePath());
    arguments.addAll(Arrays.asList(args));

    ExternalCommand cmd = ExternalCommand.create(arguments);

    cmd.setWorkingDirectory(workingDirectory);

    cmd.setRedirectErrorStream(true);

    cmd.start();

    /* Use timeout if it is a valid value! */
    if (timeout <= 0)
      cmd.waitFor();
    else
      cmd.waitFor(timeout);

    if (log.isDebugEnabled())
      log.debug(cmd.getStringOutput());

    return cmd;
  }
}


/*
 * 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.clock;

import java.io.PrintWriter;
import java.io.OutputStream;

/**
 * This class is used to measure time intervals... Each time you tick, you
 * get the amount of time ellapsed since the last one
 *
 *
 * @author ypujante@linkedin.com */
public class Chronos
{
  /**
   * Where to print information
   */
  private final PrintWriter _str;

  /**
   * The last tick
   */
  private long _tick = 0;

  /**
   * When the chronos is started  */
  private long _startTime;

  /**
   * The clock to use  */
  private final Clock _clock;

  /**
   * Constructor : initialize the tick and sets no stream */
  public Chronos()
  {
    this((PrintWriter) null);
  }

  /**
   * Constructor : initialize the tick and set the stream to the one given
   *
   * @param str the print stream for the output */
  public Chronos(PrintWriter str)
  {
    this(new SystemClock(), str);
  }

  /**
   * Constructor : initialize the tick and set the stream to the one given
   *
   * @param str the print stream for the output */
  public Chronos(OutputStream str)
  {
    this(new PrintWriter(str));
  }

  /**
   * Constructor : initialize the tick and sets no stream */
  public Chronos(Clock clock)
  {
    this(clock, (PrintWriter) null);
  }

  /**
   * Constructor : initialize the tick and set the stream to the one given
   *
   * @param str the print stream for the output */
  public Chronos(Clock clock, PrintWriter str)
  {
    _str = str;
    _clock = clock;
    _startTime = _clock.currentTimeMillis();
    tick();
  }

  /**
   * Constructor : initialize the tick and set the stream to the one given
   *
   * @param str the print stream for the output */
  public Chronos(Clock clock, OutputStream str)
  {
    this(clock, new PrintWriter(str));
  }

  /**
   * Returns the number of milliseconds ellapsed since the last call to
   * this function.
   *
   * @return the number of milliseconds since last call*/
  public long tick()
  {
    long tick = _clock.currentTimeMillis();
    long diff = tick - _tick;

    _tick = tick;

    return diff;
  }

  /**
   * @return the total time since start of this chronos */
  public long getTotalTime()
  {
    return _clock.currentTimeMillis() - _startTime;
  }

  /**
   * Display the time ellapsed since the last call (with no message) */
  public void displayElapsedTime()
  {
    this.displayElapsedTime("");
  }

  /**
   * Display the time ellapsed since the last call (with an additional
   * message)
   *
   * @param mess the message to display first */
  public void displayElapsedTime(String mess)
  {
    if(_str != null)
      _str.println(mess + getElapsedTime());
  }

  /**
   * Returns a string that represents the time elapsed
   *
   * @return the elapsed time as a string */
  public String getElapsedTime()
  {
    StringBuilder sb = new StringBuilder("Time: ");
    sb.append(this.tick());
    sb.append(" ms");

    return sb.toString();
  }

  /**
   * @return the elapsed time as hour/minute/second */
  public String getElapsedTimeAsHMS()
  {
    return new Timespan(tick()).getCanonicalString();
  }

  /**
   * Flushes the underlying writer */
  public void flush()
  {
    if(_str != null)
      _str.flush();
  }
}


/*
 * 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.linkedin.util.io.PathUtils;
import org.linkedin.util.io.resource.internal.AbstractResource;
import org.linkedin.util.io.resource.internal.InternalResourceProvider;
import org.linkedin.util.io.resource.internal.JarResourceProvider;
import org.linkedin.util.io.resource.internal.LeafResource;
import org.linkedin.util.io.resource.internal.ResourceProvider;

import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

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

  private final String _fullPath;
  private final LeafResource _jarResource;

  /**
   * Make sure we close the jar file when the input stream gets closed
   */
  private static class CloseJarFileInputStream extends FilterInputStream
  {
    private final JarFile _jarFile;

    public CloseJarFileInputStream(Resource resource, JarFile jarFile, String location) throws IOException
    {
      super(extractInputStream(jarFile, location, resource));
      _jarFile = jarFile;
    }

    private static InputStream extractInputStream(JarFile jarFile, String location, Resource resource)
      throws IOException
    {
      JarEntry jarEntry = jarFile.getJarEntry(location);

      if(jarEntry == null)
        throw new IOException("cannot get input stream for entry " + location + " for " + resource.toURI());

      if(jarEntry.getSize() == 0)
      {
        JarEntry directoryEntry =
          jarFile.getJarEntry(PathUtils.addTrailingSlash(location));
        if(directoryEntry != null)
          throw new IOException("cannot read directory for " + resource.toURI());

      }
      InputStream is = jarFile.getInputStream(jarEntry);
      if(is == null)
      {
        throw new IOException("cannot get input stream for entry " + jarEntry + " for " + resource.toURI());
      }
      return is;
    }

    @Override
    public void close() throws IOException
    {
      // YP Note: I know it looks like a bug because in general we do super.close in the finally
      // but in this case the input stream is actually associated to the jar file so we need
      // to close it first before closing the jar file...
      try
      {
        super.close();
      }
      finally
      {
        _jarFile.close();
      }
    }
  }

  /**
   * Constructor
   */
  public JarResource(InternalResourceProvider resourceProvider,
                        String path,
                        LeafResource jarResource,
                        String fullPath)
  {
    super(resourceProvider, path);

    _jarResource = jarResource;
    // the full path within the jar should not start with '/'
    _fullPath = PathUtils.removeLeadingSlash(fullPath);
  }

  /**
   * @return the content jar file (please close it when done!)
   * @throws IOException
   */
  private JarFile getContentJarFile() throws IOException
  {
    return new JarFile(_jarResource.getFile());
  }

  /**
   * 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
  {
    throw new IOException("not supported");
  }

  /**
   * @return true if the resource exists.
   */
  @Override
  public boolean exists()
  {
    // represents the root...
    if(_fullPath.equals(""))
    {
      return _jarResource.exists();
    }

    try
    {
      JarFile contentJarFile = getContentJarFile();
      try
      {
        JarEntry jarEntry = contentJarFile.getJarEntry(_fullPath);
        return jarEntry != null;
      }
      finally
      {
        contentJarFile.close();
      }
    }
    catch(IOException e)
    {
      if(log.isDebugEnabled())
        log.debug("exception (ignored) while getting entry " + _fullPath, e);

      return false;
    }
  }

  /**
   * 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
  {
    JarFile contentJarFile = getContentJarFile();
    try
    {
      CloseJarFileInputStream jarFileInputStream =
        new CloseJarFileInputStream(this, contentJarFile, _fullPath);

      // we are delegating the closure of the jar file to the input stream...
      contentJarFile = null;
      return jarFileInputStream;
    }
    finally
    {
      if(contentJarFile != null)
        contentJarFile.close();
    }
  }

  /**
   * Efficiently returns all information about the resource.
   *
   * @return information about this resource.
   * @throws IOException if cannot get information
   */
  @Override
  public ResourceInfo getInfo() throws IOException
  {
    // represents the root...
    if(_fullPath.equals(""))
    {
      return new StaticInfo(0, // root is a directory...
                            _jarResource.getInfo().getLastModified()); // last modified is the jar file itself!
    }

    JarFile contentJarFile = getContentJarFile();
    try
    {
      JarEntry jarEntry = contentJarFile.getJarEntry(_fullPath);

      if(jarEntry == null)
        throw new ResourceNotFoundException(toURI());

      return new StaticInfo(jarEntry.getSize(),
                            jarEntry.getTime());
    }
    finally
    {
      contentJarFile.close();
    }
  }

  /**
   * 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)
  {
    // for jar resource there is a big optimization... if the last modified time of the jar resource
    // itself is in the past, then there is no need to check inside the jar file at all!
    if(_jarResource.isModifiedSince(time))
    {
      return super.isModifiedSince(time);
    }
    else
      return false;
  }

  /**
   * @return true if this resource represents a directory.
   */
  @Override
  public boolean isDirectory()
  {
    // represents the root...
    if(_fullPath.equals(""))
    {
      return true;
    }

    try
    {
      String fullPath = PathUtils.removeTrailingSlash(_fullPath);

      JarFile contentJarFile = getContentJarFile();
      try
      {
        JarEntry entry = contentJarFile.getJarEntry(fullPath);

        if(entry != null && entry.getSize() == 0)
        {
          // we know that a directory reports a size of 0, so we need to check whether it is
          // actually a directory or a file of size 0...
          JarEntry directoryEntry =
            contentJarFile.getJarEntry(PathUtils.addTrailingSlash(fullPath));
          if(directoryEntry != null)
            return true;
        }

        return false;
      }
      finally
      {
        contentJarFile.close();
      }
    }
    catch(IOException e)
    {
      if(log.isDebugEnabled())
        log.debug("exception (ignored) while getting entry " + _fullPath, e);

      return false;
    }
  }

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

  /**
   * @return a uri representation of the resource
   */
  private URI toURI(String location)
  {
    StringBuilder sb = new StringBuilder("jar:");
    sb.append(_jarResource.toURI().toString());
    sb.append("!/");
    sb.append(location);

    return URI.create(sb.toString());
  }

  /**
   * Factory method.
   *
   * @param uri the uri to the jar... note that it can either be jar: or a plain uri to the
   * jar (ex: file://tmp/foo.jar and jar:file://tmp/foo.jar!/ are both valid and pointing to the
   * same resource!)
   */
  public static Resource create(URI uri) throws URISyntaxException, IOException
  {
    return create(uri, URIResourceFactory.DEFAULT);
  }

  /**
   * Factory method.
   *
   * @param uri the uri to the jar... note that it can either be jar: or a plain uri to the
   * jar (ex: file://tmp/foo.jar and jar:file://tmp/foo.jar!/ are both valid and pointing to the
   * same resource!)
   */
  public static Resource create(URI uri, URIResourceFactory factory)
    throws URISyntaxException
  {
    String path = "/";

    // case when the URI is already a jar uri
    if("jar".equals(uri.getScheme()))
    {
      String schemeSpecificPart = uri.getSchemeSpecificPart();

      if(schemeSpecificPart == null)
        throw new URISyntaxException(uri.toString(), "no scheme specific part found...");

      int idx = schemeSpecificPart.indexOf("!/");
      if(idx == -1)
        throw new URISyntaxException(uri.toString(), "couldn't find !/");

      uri = new URI(schemeSpecificPart.substring(0, idx));
      path = schemeSpecificPart.substring(idx + 1);
    }

    // case when the URI points directly to the jar resource
    try
    {
      ResourceProvider provider =
        new JarResourceProvider(factory.createResource(uri));

      return provider.createResource("/").createRelative(path);
    }
    catch(UnsupportedURIException e)
    {
      URISyntaxException ex = new URISyntaxException(uri.toString(), "cannot create resource");
      ex.initCause(e);
      throw ex;
    }
  }

  /**
   * Convenient call to create a resource from the jar resource (points at the root of the
   * jar...)
   *
   * @param jarResource
   * @return the resource pointing at the root of the jar
   */
  public static Resource create(Resource jarResource)
  {
    return create(jarResource, "/");
  }

  /**
   * Convenient call to create a resource from the jar resource (points at rootPath in the
   * jar...)
   *
   * @param jarResource
   * @param rootPath where is the root in the jar
   * @return the resource with root at rootPath
   */
  public static Resource create(Resource jarResource, String rootPath)
  {
    return new JarResourceProvider(jarResource, rootPath).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.xml;

import java.util.Map;

/**
 * Define some methods in terms of others so that there is less code to
 * implement in further implementation.
 *
 * @author  ypujante@linkedin.com */
public abstract class AbstractXMLBuilder implements XMLBuilder
{
  /**
   * Constructor */
  public AbstractXMLBuilder()
  {
  }

  /**
   * Adds a tag
   *
   * @param tagName the name of the tag to add
   * @param value the value of the tag to add */
  @Override
  public void addTag(String tagName, int value)
  {
    addTag(tagName, String.valueOf(value));
  }

  /**
   * Adds a tag
   *
   * @param tagName the name of the tag to add
   * @param value the value of the tag to add */
  @Override
  public void addTag(String tagName, double value)
  {
    addTag(tagName, String.valueOf(value));
  }

  /**
   * Adds a tag which contains an attribute
   *
   * @param tagName the name of the tag to add
   * @param value the value of the tag to add
   * @param attrName the name of the attribute
   * @param attrValue the value of the attribute */
  @Override
  public void addTag(String tagName,
                     int value,
                     String attrName,
                     String attrValue)
  {
    addTag(tagName, String.valueOf(value), attrName, attrValue);
  }

  /**
   * Adds a tag which contains an attribute
   *
   * @param tagName the name of the tag to add
   * @param value the value of the tag to add
   * @param attrName the name of the attribute
   * @param attrValue the value of the attribute */
  @Override
  public void addTag(String tagName,
                     double value,
                     String attrName,
                     String attrValue)
  {
    addTag(tagName, String.valueOf(value), attrName, attrValue);
  }

  /**
   * Adds a tag which contains the attributes specified. All the
   * properties will be turned into attributes.
   *
   * @param tagName the name of the tag to add
   * @param value the value of the tag to add
   * @param attributes the attributes */
  @Override
  public void addTag(String tagName, int value, Map attributes)
  {
    addTag(tagName, String.valueOf(value), attributes);
  }

  /**
   * Adds a tag which contains the attributes specified. All the
   * properties will be turned into attributes.
   *
   * @param tagName the name of the tag to add
   * @param value the value of the tag to add
   * @param attributes the attributes */
  @Override
  public void addTag(String tagName, double value, Map attributes)
  {
    addTag(tagName, String.valueOf(value), attributes);
  }
}