/*
 * 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.io.StringReader;
import java.io.BufferedReader;
import java.io.IOException;

/**
 * This class encapsulates the notion of indentation. It maintains
 * internally a 'level' of indentation that you should modify with
 * inc or dec. You can also specify which
 * characters will be used to display the indentation (most of the time it
 * will be spaces).
 *
 *
 * @author ypujante@linkedin.com */
public class Indent
{
  /**
   * The level of indentation */
  private int _level;

  /**
   * The String that will be used to display the indentation */
  private String _indent_string;

  private String _indentation = null;
  /**
   * Constructor. Initializes the indentation to start at level 0 and to
   * have a display of 2 spaces for each level. */
  public Indent()
  {
    this("  ", 0);
  }

  /**
   * Constructor. Initializes with the specified string and a starting
   * level of 0.
   *
   * @param indent_string the indentation representation */
  public Indent(String indent_string)
  {
    this(indent_string, 0);
  }

  /**
   * Constructor. Initializes with the specified level and a 2 spaces string.
   *
   * @param level the level of indetation to start */
  public Indent(int level)
  {
    this("  ", level);
  }

  /**
   * Constructor. Initializes with provided values.
   *
   * @param indent_string the indentation representation
   * @param level the level of indentation to start */
  public Indent(String indent_string, int level)
  {
    _indent_string = indent_string;
    _level = level;
    calculateIndentationString();
  }

  /**
   * Access function.
   *
   * @return the string representation of the indentation */
  public String getIndentString()
  {
    return _indent_string;
  }

  /**
   * Access function.
   *
   * @param indent_string the string representation of the indentation */
  public void setIndentString(String indent_string)
  {
    _indent_string = indent_string;
    calculateIndentationString();
  }

  /**
   * Access function.
   *
   * @return the indentation level */
  public int getLevel()
  {
    return _level;
  }

  /**
   * Access function. You should not use this method to modify the level of
   * indentation but use inc or dec instead.
   *
   * @param level the indentation level */
  public void setLevel(int level)
  {
    _level = level;
    if(_level < 0)
      _level = 0;

    calculateIndentationString();
  }

  /**
   * Increments the level of indentation */
  public void inc()
  {
    _level++;
    calculateIndentationString();
  }

  /**
   * Decrements the level of indentation */
  public void dec()
  {
    _level--;
    if(_level < 0) {
      _level = 0;
  }
    calculateIndentationString();
  }

  /**
   * Returns the indentation depending on the level.
   *
   * @return the indentation */
  public String getIndentation()
  {
    return _indentation;
  }

  private void calculateIndentationString() {
    if(_level == 0) {
      _indentation = "";
      return;
    }

    // simple optim
    if(_level == 1) {
      _indentation = _indent_string;
      return;
    }

    StringBuilder sb = new StringBuilder(_level + _indent_string.length());

    for(int i = 0; i < _level; i++)
      sb.append(_indent_string);

    _indentation = sb.toString();
  }

  /**
   * Convenient method : returns the indentation depending on the level.
   *
   * @return the indentation */
  public String toString()
  {
    return getIndentation();
  }

  /**
   * Indents a block of text. You provide the block of text as a String and
   * the indent object and it returns another String with each line
   * properly indented.
   *
   * @param block the block of text to indent
   * @param indent the indentation object to use
   * @return the indented block */
  public static String indentBlock(String block, Indent indent)
  {
    StringBuilder sb = new StringBuilder();

    BufferedReader br = new BufferedReader(new StringReader(block));
    String line;
    try
    {
      while((line = br.readLine()) != null)
        sb.append(indent).append(line).append('\n');
    }
    catch(IOException ex)
    {
      // on a String ? I doubt...
    }

    return sb.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 org.linkedin.util.io.ram.RAMDirectory;
import org.linkedin.util.io.ram.RAMEntry;
import org.linkedin.util.io.ram.RAMFile;
import org.linkedin.util.io.resource.internal.AbstractResource;
import org.linkedin.util.io.resource.internal.RAMResourceProvider;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * @author ypujante@linkedin.com
 *
 */
public class RAMResource extends AbstractResource
{
  private final String _fullPath;
  private final RAMResourceProvider _resourceProvider;

  /**
   * Constructor
   */
  public RAMResource(RAMResourceProvider resourceProvider,
                        String fullPath,
                        String path)
  {
    super(resourceProvider, path);
    _resourceProvider = resourceProvider;
    _fullPath = fullPath;
  }

  /**
   * @return the ram entry
   */
  public RAMEntry getRAMEntry()
  {
    return _resourceProvider.getRAMEntry(_fullPath);
  }

  /**
   * @return true if the resource exists.
   */
  @Override
  public boolean exists()
  {
    return getRAMEntry() != null;
  }

  /**
   * 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 a 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
  {
    RAMEntry entry = getRAMEntry();

    if(entry == null || entry instanceof RAMDirectory)
      throw new ResourceNotFoundException(toURI());

    return new ByteArrayInputStream(((RAMFile) entry).getContent());
  }

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

    if(entry != null)
      return new StaticInfo(entry.getContentLength(),
                            entry.lastModified());
    else
      throw new ResourceNotFoundException(toURI());
  }

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

  /**
   * @return a uri representation of the resource
   */
  @Override
  public URI toURI()
  {
    try
    {
      return new URI("ram://" + _fullPath);
    }
    catch(URISyntaxException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }
  }

  /**
   * Factory of {@link RAMResource} (based on a {@link RAMDirectory}
   *
   * @param root
   * @return resource
   */
  public static Resource create(RAMDirectory root)
  {
    return new RAMResourceProvider(root).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.clock;

/**
 * @author ypujante@linkedin.com
 *
 */
public class ClockUtils
{
  /**
   * Converts the object into a timespan. If already a timespan then simply return the object.
   * Otherwise call {@link Timespan#parse(String)} on t.toString(). Works well for
   * long and String
   *
   * @return a timespan (null if t is null)
   */
  public static Timespan toTimespan(Object t)
  {
    if(t == null)
      return null;

    if(t instanceof Timespan)
    {
      return (Timespan) t;
    }

    return Timespan.parse(t.toString());
  }

  /**
   * Computes the end time = now + timeout
   *
   * @param timeout (see {@link #toTimespan(Object)})
   */
  public static long toEndTime(Object timeout)
  {
    return toEndTime(null, timeout);
  }

  /**
   * Computes the end time = now + timeout
   *
   * @param clock can be null to use system clock
   * @param timeout (see {@link #toTimespan(Object)})
   * @return 0 if
   */
  public static long toEndTime(Clock clock, Object timeout)
  {
    Timespan t = toTimespan(timeout);
    if(t == null)
      return 0;
    if(clock == null)
      clock = SystemClock.INSTANCE;

    if(t.getDurationInMilliseconds() == 0)
      return 0;

    return t.futureTimeMillis(clock);
  }

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


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

import org.linkedin.util.clock.Clock;
import org.linkedin.util.clock.SystemClock;
import org.linkedin.util.io.resource.RAMResource;
import org.linkedin.util.io.resource.Resource;

import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.FileNotFoundException;

/**
 * @author ypujante@linkedin.com
 *
 */
public class RAMDirectory extends RAMEntry
{
  private final Map<String, RAMEntry> _directoryContent;

  public RAMDirectory()
  {
    this(SystemClock.instance(), "");
  }

  public RAMDirectory(Clock clock)
  {
    this(clock, "");
  }

  public RAMDirectory(Clock clock,
                      String name,
                      Map<String, RAMEntry> directoryContent)
  {
    super(clock, name);
    _directoryContent = directoryContent;
  }

  public RAMDirectory(Clock clock,
                      String name,
                      long lastModifiedDate,
                      Map<String, RAMEntry> directoryContent)
  {
    super(clock, name, lastModifiedDate);
    _directoryContent = directoryContent;
  }

  /**
   * Creates empty directory
   *
   * @param lastModifiedDate
   */
  public RAMDirectory(Clock clock, String name, long lastModifiedDate)
  {
    this(clock, name, lastModifiedDate, new HashMap<String, RAMEntry>());
  }

  /**
   * Creates empty directory
   */
  public RAMDirectory(Clock clock, String name)
  {
    this(clock, name, new HashMap<String, RAMEntry>());
  }

  public RAMDirectory cd(String path) throws FileNotFoundException
  {
    RAMEntry ramEntry = getEntryByPath(path);
    if(ramEntry instanceof RAMDirectory)
      return (RAMDirectory) ramEntry;
    else
      throw new FileNotFoundException(path);
  }

  public RAMEntry getEntry(String name)
  {
    return _directoryContent.get(name);
  }

  /**
   * @param path
   * @return the entry or null if not found
   */
  public RAMEntry getEntryByPath(String path)
  {
    RAMEntry entry = this;

    String[] pathComponents = path.split("/");

    for(String pathComponent : pathComponents)
    {
      // we skip empty path components (caused by leading / + double //)
      if(pathComponent.equals(""))
        continue;

      if(entry instanceof RAMDirectory)
      {
        RAMDirectory ramDirectory = (RAMDirectory) entry;
        entry = ramDirectory.getEntry(pathComponent);
      }
      else
      {
        entry = null;
      }

      if(entry == null)
        break;
    }

    return entry;
  }

  @Override
  public long getContentLength()
  {
    return 0;
  }

  /**
   * Equivalent to 'touch' unix command: updates the last modified date to the value provided
   * and if the entry does not create it, it creates an empty file!
   *
   * @param name
   * @return the entry (either a new file or the entry that was modified)
   */
  public RAMEntry touch(String name)
  {
    return touch(name, _clock.currentTimeMillis());
  }

  /**
   * Equivalent to 'touch' unix command: updates the last modified date to the value provided
   * and if the entry does not create it, it creates an empty file!
   *
   * @param name
   * @param lastModifiedDate
   * @return the entry (either a new file or the entry that was modified)
   */
  public RAMEntry touch(String name, long lastModifiedDate)
  {
    RAMEntry ramEntry = _directoryContent.get(name);
    if(ramEntry == null)
    {
      ramEntry = add(new RAMFile(_clock, name, new byte[0]));
    }

    ramEntry.touch(lastModifiedDate);

    return ramEntry;
  }

  /**
   * Copy the entry in this directory with the provided name
   *
   * @param entry
   * @return the touched entry
   */
  public RAMEntry add(RAMEntry entry)
  {
    touch();
    _directoryContent.put(entry.name(), entry);
    return entry;
  }

  /**
   * Creates a file with the content and add it to the directory with this name
   *
   * @param name
   * @return the created file
   */
  public RAMFile add(String name, byte[] content)
  {
    return (RAMFile) add(new RAMFile(_clock, name, content));
  }

  /**
   * Creates a file with the content and add it to the directory with this name
   *
   * @param name
   * @param content as a string
   * @return the created file
   */
  public RAMFile add(String name, String content)
  {
    try
    {
      return add(name, content.getBytes("UTF-8"));
    }
    catch(UnsupportedEncodingException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }
  }

  /**
   * Creates an empty directory and add it to this directory with this name. If the directory
   * already exists, it does not recreate it.
   *
   * @param name
   * @return the created directory
   * @throws IOException if the name represents a file
   */
  public RAMDirectory mkdir(String name) throws IOException
  {
    RAMEntry entry = getEntry(name);
    if(entry instanceof RAMDirectory)
    {
      RAMDirectory ramDirectory = (RAMDirectory) entry;
      return ramDirectory;
    }
    else
    {
      if(entry == null)
      {
        RAMDirectory directory = new RAMDirectory(_clock, name);
        return (RAMDirectory) add(directory);
      }
      else
      {
        throw new IOException("File exists: " + name);
      }
    }
  }

  /**
   * Removes the entry (whether it is a directory or not)
   *
   * @param name
   * @return the entry removed (null if did not exist)
   */
  public RAMEntry rm(String name)
  {
    return _directoryContent.remove(name);
  }

  /**
   * Creates an empty directory and add it to this directory with this path (equivalent
   * to unix command): create all intermediary directories
   *
   * @param path
   * @return the created directory
   */
  public RAMDirectory mkdirhier(String path) throws IOException
  {
    RAMDirectory directory = this;

    String[] pathComponents = path.split("/");

    for(String pathComponent : pathComponents)
    {
      // we skip empty path components (caused by leading / + double //)
      if(pathComponent.equals(""))
        continue;

      RAMEntry entry = directory.getEntry(pathComponent);

      if(entry == null)
      {
        directory = directory.mkdir(pathComponent);
      }
      else
      {
        if(entry instanceof RAMDirectory)
        {
          directory = (RAMDirectory) entry;
        }
        else
        {
          throw new IOException("File exists: " + pathComponent);
        }
      }
    }

    return directory;
  }

  /**
   * @return the content of the directory
   */
  public Collection<RAMEntry> ls()
  {
    return _directoryContent.values();
  }

  /**
   * @return this object wrapped in a resource
   */
  public Resource toResource()
  {
    return RAMResource.create(this);
  }
}


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

package org.linkedin.util.io.resource.internal;

import org.linkedin.util.io.resource.FileResource;
import org.linkedin.util.io.resource.Resource;
import org.linkedin.util.io.resource.URIResourceFactory;

import java.io.File;
import java.net.URI;

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

  /**
   * @return the protocol that this factory handles
   */
  @Override
  public String getScheme()
  {
    return "file";
  }

  /**
   * 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)
  {
    return FileResource.create(new File("/"), uri.getPath());
  }
}


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

import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

/**
 * Implements a 'chain' of resource providers... it will delegate to a list of providers.
 * The order is important as the first one which exists will win.
 *
 * @author ypujante@linkedin.com
 *
 */
public class ResourceProviderChain extends AbstractResourceProvider
{
  private final List<InternalResourceProvider> _resourceProviders;

  /**
   * Constructor
   */
  public ResourceProviderChain(InternalResourceProvider... resourceProviders)
  {
    this(Arrays.asList(resourceProviders));
  }

  /**
   * Constructor
   */
  public ResourceProviderChain(List<InternalResourceProvider> resourceProviders)
  {
    if(resourceProviders.size() == 0)
      throw new IllegalArgumentException("list is emtpy...");

    _resourceProviders = resourceProviders;
  }

  /**
   * @return the internal list...
   */
  public List<InternalResourceProvider> getResourceProviders()
  {
    return Collections.unmodifiableList(_resourceProviders);
  }

  /**
   * 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)
  {
    // since the list is not empty, this variable will never remain null...
    InternalResource resource = null;

    for(InternalResourceProvider resourceProvider : _resourceProviders)
    {
      resource = resourceProvider.doBuildResource(path);
      if(resource.exists())
        break;
    }

    return new ResourceChain(this, path, resource);
  }

  /**
   * Creates a new resource provider given the new path.
   *
   * @param rootPath
   * @return the new resource provider
   */
  @Override
  public InternalResourceProvider doCreateResourceProvider(String rootPath)
  {
    List<InternalResourceProvider> resourceProviders =
      new ArrayList<InternalResourceProvider>(_resourceProviders.size());

    for(InternalResourceProvider resourceProvider : _resourceProviders)
    {
      resourceProviders.add(resourceProvider.doCreateResourceProvider(rootPath));
    }

    return new ResourceProviderChain(resourceProviders);
  }

  /**
   * 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, final ResourceFilter filter)
  {
    boolean isDirectory = false;

    ResourceFilter newFilter = new ResourceFilter()
    {
      @Override
      public boolean accept(Resource resource)
      {
        return filter.accept(new ResourceChain(ResourceProviderChain.this,
                                               resource.getPath(),
                                               resource));
      }
    };

    for(InternalResourceProvider resourceProvider : _resourceProviders)
    {
      isDirectory |= resourceProvider.doList(path, newFilter);
    }

    return isDirectory;
  }

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

    ResourceProviderChain that = (ResourceProviderChain) o;

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

    return true;
  }

  @Override
  public int hashCode()
  {
    return _resourceProviders.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.codec;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;

/**
 * @author ypujante@linkedin.com
 *
 */
public class CodecUtils
{
  /**
   * Encodes the string using the codec provided.
   *
   * @param codec encode the string
   * @param s the string to encode
   * @return the encoded string */
  public static String encodeString(OneWayCodec codec, String s)
  {
    try
    {
      return codec.encode(s.getBytes("UTF-8"));
    }
    catch(UnsupportedEncodingException ex)
    {
      // shouldn't happen
      throw new RuntimeException(ex);
    }
  }

  /**
   * Decodes the string using the codec provided. Returns a string
   *
   * @param codec encode the string
   * @param s the string to encode
   * @return the encoded string
   * @throws Codec.CannotDecodeException if cannot decode the string */
  public static String decodeString(Codec codec, String s)
    throws Codec.CannotDecodeException
  {
    try
    {
      return new String(codec.decode(s), "UTF-8");
    }
    catch(UnsupportedEncodingException ex)
    {
      // shouldn't happen
      throw new RuntimeException(ex);
    }
  }

  /**
   * Create a Random object by extracting the seed from the password.
   */
  public static Random createRandom(String password)
  {
    if(password == null)
      return null;

    byte[] bytes;

    try
    {
      MessageDigest md = MessageDigest.getInstance("SHA-1");
      bytes = md.digest(password.getBytes("UTF-8"));
    }
    catch(NoSuchAlgorithmException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }
    catch(UnsupportedEncodingException e)
    {
      // should not happen
      throw new RuntimeException(e);
    }

    int len = bytes.length;

    // we only take at most 6 bytes => 48 bits
    if(len > 6)
      len = 6;

    long seed = 0;

    if(len > 0)
    {
      seed = bytes[0];

      for(int i = 1; i < len; i++)
      {
        seed <<=  8;
        seed |= bytes[i];
      }
    }

    int k = 0;

    for(int i = len; i < bytes.length; i++)
    {
      seed ^= (((long) bytes[i]) << (k * 8));
      if(k++ == 5)
        k = 0;
    }

    return new Random(seed);
  }

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


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

/**
 * @author ypujante@linkedin.com
 *
 */
public class TextUtils
{

  /**
   * @param s the string to test
   * @return true iff s is null or if s contains only whitespace.
   */
  public static boolean isEmptyString(String s)
  {
    return s == null || s.trim().length() == 0;
  }

  /**
   * Searches in the string 's' all occurences of the
   * substring 'src' and replaces it with the string
   * 'dst'.
   *
   * @param s the string to search
   * @param src the substring to search for inside s
   * @param dst the replacing string
   * @return the string with the text replaced */
  public static String searchAndReplace(String s, String src, String dst)
  {
    if(s == null)
      return null;

    StringBuilder result = new StringBuilder();

    int i = 0;
    int len = s.length();
    int len2 = src.length();

    while(i < len)
    {
      int index = s.indexOf(src, i);
      if(index == -1)
      {
        if(i == 0)
          return s;

        result.append(s.substring(i));
        break;
      }

      result.append(s.substring(i, index));
      result.append(dst);

      i = index + len2;
    }

    return result.toString();
  }

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


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

package org.linkedin.util.reflect;

import org.slf4j.Logger;
import org.linkedin.util.collections.CompoundEnumeration;

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

/**
 * Chains multiple class loaders.
 *
 * @author ypujante@linkedin.com
 *
 */
public class ClassLoaderChain extends ClassLoader
{
  public static final String MODULE = ClassLoaderChain.class.getName();
  public static final Logger log = org.slf4j.LoggerFactory.getLogger(MODULE);

  private final Collection<ClassLoader> _classLoaders;

  private ClassLoaderChain(Collection<ClassLoader> classLoaders)
  {
    _classLoaders = classLoaders;
  }

  @Override
  public URL getResource(String name)
  {
    URL url = null;
    for(ClassLoader classLoader : _classLoaders)
    {
      url = classLoader.getResource(name);

      if(url != null)
        return url;
    }
    return url;
  }

  @Override
  @SuppressWarnings("unchecked")
  public Enumeration<URL> getResources(String name) throws IOException
  {
    Enumeration<URL>[] enums = new Enumeration[_classLoaders.size()];

    int i = 0;
    for(ClassLoader classLoader : _classLoaders)
    {
      enums[i++] = classLoader.getResources(name);
    }

    return new CompoundEnumeration<URL>(enums);
  }

  @Override
  public Class loadClass(String name) throws ClassNotFoundException
  {
    Class clazz = null;
    for(ClassLoader classLoader : _classLoaders)
    {
      try
      {
        clazz = classLoader.loadClass(name);
        return clazz;
      }
      catch(ClassNotFoundException e)
      {
        if(log.isDebugEnabled())
          log.debug("class not found in " + classLoader + "... moving to next one");
      }
    }

    throw new ClassNotFoundException(name);
  }

  /**
   * Creates a chain of class loaders. Handles null properly. This will create a new chain, even if
   * there is only one classloader in the chain. The usage for this is that ResourceBundle caches resource per
   * classloader.
   * @param classLoaders
   * @return a class loader (never null)
   */
  public static ClassLoader createNewChain(ClassLoader... classLoaders)
  {
    return createChain(Arrays.asList(classLoaders), true);
  }

  /**
   * Creates a chain of class loaders. Handles null properly.
   * @param classLoaders
   * @return a class loader (never null)
   */
  public static ClassLoader createChain(ClassLoader... classLoaders)
  {
    return createChain(Arrays.asList(classLoaders), false);
  }

  /**
   * Creates a chain of class loaders. Handles null properly.
   * @param classLoaders
   * @return a class loader (never null)
   */
  public static ClassLoader createChain(List<ClassLoader> classLoaders)
  {
	  return createChain(classLoaders, false);
  }

  /**
   * Creates a chain of class loaders. Handles null properly.
   * @param classLoaders
   * @param createNew whether to create new instance of classloaderchain even if there is only one loader
   * @return a class loader (never null)
   */
  public static ClassLoader createChain(List<ClassLoader> classLoaders, Boolean createNew)
  {
    List<ClassLoader> list = new ArrayList<ClassLoader>(classLoaders.size());

    for(ClassLoader classLoader : classLoaders)
    {
      // we skip null class loaders
      if(classLoader == null)
        continue;

      // we skip null class loaders
      if(classLoader instanceof NullClassLoader)
        continue;

      // we 'flatten' the chain... no need to have a chain of a chain...
      if(classLoader instanceof ClassLoaderChain)
      {
        ClassLoaderChain classLoaderChain = (ClassLoaderChain) classLoader;
        list.addAll(classLoaderChain._classLoaders);
      }
      else
      {
        list.add(classLoader);
      }
    }

    if(list.size() == 0)
      return NullClassLoader.instance();

    // remove duplicates
    ClassLoader previous = null;
    Iterator<ClassLoader> iter = list.iterator();
    while(iter.hasNext())
    {
      ClassLoader next = iter.next();

      if(next.equals(previous))
        iter.remove();

      previous = next;
    }

    //TODO: create a new entry point, and keeping old behavior unchanged.
    if(list.size() == 1 && !createNew)
      return list.get(0);

    return new ClassLoaderChain(list);
  }
}


/*
 * 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.GCFile;
import org.linkedin.util.io.IOUtils;
import org.linkedin.util.io.resource.Resource;
import org.linkedin.util.io.resource.ResourceInfo;
import org.linkedin.util.io.resource.StaticInfo;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedInputStream;
import java.net.URI;

/**
 * This implementation caches the resource locally (file): the input stream gets copied into
 * a local file and the info about the remote resource is cached as well.
 *
 * @author ypujante@linkedin.com
 *
 */
public class LocalCacheLeafResource implements LeafResource
{
  private final Resource _resource;

  private File _localFile = null;
  private StaticInfo _resourceInfo = null;
  private IOException _ioException = null;

  /**
   * Constructor
   */
  public LocalCacheLeafResource(Resource resource)
  {
    _resource = resource;
  }

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

  /**
   * @return true if the resource exists.
   */
  @Override
  public boolean exists()
  {
    init();
    return _resourceInfo.getLastModified() > 0;
  }

  /**
   * 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
  {
    init();

    if(_ioException != null)
      throw _ioException;

    return _localFile;
  }

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

    if(_ioException != null)
      throw _ioException;

    return _resourceInfo;
  }

  /**
   * 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()
  {
    init();
    return _resourceInfo.getLastModified();
  }

  /**
   * 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()
  {
    init();
    return _resourceInfo.getContentLength();
  }

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

  /**
   * @return the filename portion of this resource. If it is a directory, returns the name of the
   *         directory.
   */
  @Override
  public String getFilename()
  {
    return _resource.getFilename();
  }

  /**
   * @return the path to the resource (within the context of the resource provider)
   */
  @Override
  public String getPath()
  {
    return _resource.getPath();
  }

  /**
   * Initializes the fields (safe to call multiple times). Thread safe.
   */
  private synchronized void init()
  {
    // already initialized
    if(_resourceInfo != null)
      return;

    // make a local copy of the file
    try
    {
      InputStream is = _resource.getInputStream();
      try
      {
        File file = GCFile.createTempFile(LocalCacheLeafResource.class.getName(),
                                          _resource.getFilename());
        FileOutputStream fos = new FileOutputStream(file);
        try
        {
          BufferedOutputStream out = new BufferedOutputStream(fos);
          IOUtils.copy(new BufferedInputStream(is), out);
          out.flush();
        }
        finally
        {
          fos.close();
        }

        ResourceInfo info = _resource.getInfo();

        _localFile = file;
        _resourceInfo = new StaticInfo(info.getContentLength(), info.getLastModified());
        _ioException = null;
      }
      finally
      {
        is.close();
      }
    }
    catch(IOException e)
    {
      _localFile = null;
      _resourceInfo = new StaticInfo(0, 0);
      _ioException = e;
    }

  }

  /**
   * Decorates the resource to implement a caching strategy.
   *
   * @param resource the resource to cache locally
   * @return the (locally cached) resource
   */
  public static LeafResource create(Resource resource)
  {
    try
    {
      // if we can access the file then there is no reason to decorate it...
      resource.getFile();
      return new LeafResourceImpl(resource);
    }
    catch(IOException e)
    {
      return new LocalCacheLeafResource(resource);
    }
  }

}


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

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.EnumSet;
import java.util.Properties;

/**
 * Contains useful global utils methods.
 *
 * @author ypujante@linkedin.com */
public class CollectionsUtils
{
  /**
   * Reverses the array in place.
   * @param array the array to reverse
   * @return the same array provided NOT a new one!
   */
  public static <T> T[] reverse(T[] array)
  {
    if(array == null)
      return array;

    int s = 0;
    int e = array.length - 1;
    while(s < e)
    {
      // swap index e and s
      T tmp = array[e];
      array[e] = array[s];
      array[s] = tmp;

      s++;
      e--;
    }

    return array;
  }

  /**
   * Turns an array of enumeration values into an enum set
   *
   * @param clazz the type of the enum
   * @param ts the array of enums
   * @return the enum set containing all the values from the array
   */
  public static <T extends Enum<T>> EnumSet<T> toEnumSet(Class<T> clazz, T... ts)
  {
    if(ts == null)
      return null;

    EnumSet<T> res = EnumSet.noneOf(clazz);
    for(T t : ts)
    {
      res.add(t);
    }
    return res;
  }

  /**
   * Convenient call to load a properties file from the provided file
   */
  public static Properties loadProperties(File file) throws IOException
  {
    if(file == null)
      return null;

    FileReader reader = new FileReader(file);
    try
    {
      return loadProperties(reader);
    }
    finally
    {
      reader.close();
    }
  }

  /**
   * Convenient call to load a properties file from the provided reader
   */
  public static Properties loadProperties(Reader reader) throws IOException
  {
    if(reader == null)
      return null;

    Properties properties = new Properties();
    properties.load(reader);
    return properties;
  }
}


/*
 * 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 org.linkedin.util.collections.CollectionsUtils;
import org.linkedin.util.lang.LangUtils;

import java.io.Serializable;
import java.util.Date;
import java.util.EnumMap;
import java.util.EnumSet;

/**
 * This class represents a notion of timespan. Note that the time units goes from milliseconds to year.
 * It does not go below milliseconds because we rarely use this kind of precision especially since the
 * java vm does not support it very well. Note that above hour, the timespan is approximate. This
 * object is immutable and thread safe.
 *
 * @author ypujante@linkedin.com
 *
 */
public class Timespan implements Comparable<Timespan>, Serializable
{
  private static final long serialVersionUID = 1L;

  public enum TimeUnit
  {
    MILLISECOND(1L, ""),
    SECOND     (1000L * MILLISECOND.getMillisecondsCount(), "s"),
    MINUTE     (60L * SECOND.getMillisecondsCount(), "m"),
    HOUR       (60L * MINUTE.getMillisecondsCount(), "h"),
    // note that the values below are approximations, and should not be used to compute exact
    // values!
    DAY        (24L * HOUR.getMillisecondsCount(), "d"),
    WEEK       (7L * DAY.getMillisecondsCount(), "w"),
    MONTH       (30L * DAY.getMillisecondsCount(), "M"),
    YEAR       (365L * DAY.getMillisecondsCount(), "y");

    private final long _millisecondsCount;
    private final String _displayChar;

    private TimeUnit(long millisecondsCount, String displayChar)
    {
      _millisecondsCount = millisecondsCount;
      _displayChar = displayChar;
    }

    public long getMillisecondsCount()
    {
      return _millisecondsCount;
    }

    public String getDisplayChar()
    {
      return _displayChar;
    }
  }

  public final static Timespan ZERO_YEARS = new Timespan(0, TimeUnit.YEAR);
  public final static Timespan ZERO_MONTHS = new Timespan(0, TimeUnit.MONTH);
  public final static Timespan ZERO_WEEKS = new Timespan(0, TimeUnit.WEEK);
  public final static Timespan ZERO_DAYS = new Timespan(0, TimeUnit.DAY);
  public final static Timespan ZERO_HOURS = new Timespan(0, TimeUnit.HOUR);
  public final static Timespan ZERO_MINUTES = new Timespan(0, TimeUnit.MINUTE);
  public final static Timespan ZERO_SECONDS = new Timespan(0, TimeUnit.SECOND);
  public final static Timespan ZERO_MILLISECONDS = new Timespan(0, TimeUnit.MILLISECOND);

  public final static Timespan ONE_SECOND = new Timespan(1, TimeUnit.SECOND);
  public final static Timespan ONE_MINUTE = new Timespan(1, TimeUnit.MINUTE);

  private final static EnumMap<TimeUnit, Timespan> ZERO_TIMESPANS =
    new EnumMap<TimeUnit, Timespan>(TimeUnit.class);

  private final static TimeUnit[] TIME_UNIT_ORDER = CollectionsUtils.reverse(TimeUnit.values());

  static
  {
    ZERO_TIMESPANS.put(TimeUnit.YEAR, ZERO_YEARS);
    ZERO_TIMESPANS.put(TimeUnit.MONTH, ZERO_MONTHS);
    ZERO_TIMESPANS.put(TimeUnit.WEEK, ZERO_WEEKS);
    ZERO_TIMESPANS.put(TimeUnit.DAY, ZERO_DAYS);
    ZERO_TIMESPANS.put(TimeUnit.HOUR, ZERO_HOURS);
    ZERO_TIMESPANS.put(TimeUnit.MINUTE, ZERO_MINUTES);
    ZERO_TIMESPANS.put(TimeUnit.SECOND, ZERO_SECONDS);
    ZERO_TIMESPANS.put(TimeUnit.MILLISECOND, ZERO_MILLISECONDS);
  }

  private final static EnumSet<TimeUnit> CANONICAL_TIME_UNITS =
    EnumSet.range(TimeUnit.MILLISECOND, TimeUnit.YEAR);

  private final long _duration;
  private final TimeUnit _timeUnit;

  /**
   * Constructor
   * @param durationInMilliseconds the duration in milliseconds
   */
  public Timespan(long durationInMilliseconds)
  {
    this(durationInMilliseconds, TimeUnit.MILLISECOND);
  }

  /**
   * Constructor
   */
  public Timespan(long duration, TimeUnit timeUnit)
  {
    _duration = duration;
    _timeUnit = timeUnit;
  }

  /**
   * @return the time unit of this timespan
   */
  public TimeUnit getTimeUnit()
  {
    return _timeUnit;
  }

  /**
   * @return the duration of this timespan
   */
  public long getDuration()
  {
    return _duration;
  }

  /**
   * Adds another timespan to this timespan and return a brand new one. Note that the unit is
   * preserved if other has the same unit as 'this'.
   *
   * @param other the timespan to add
   * @return a brand new timespan.
   */
  public Timespan add(Timespan other)
  {
    if(getTimeUnit() == other.getTimeUnit())
      return new Timespan(getDuration() + other.getDuration(), getTimeUnit());

    return new Timespan(getDurationInMilliseconds() + other.getDurationInMilliseconds(),
                        TimeUnit.MILLISECOND);
  }

  /**
   * Creates and returns a new timespan whose duration is {@code this}
   * timespan's duration minus the {@code other} timespan's duration.
   *

   * The time unit is preserved if {@code other} has the same unit
   * as {@code this}.
   *

   * Negative timespans are not supported, so if the {@code other}
   * timespan duration is greater than {@code this} timespan duration,
   * a timespan of zero is returned (i.e., a negative timespan is never
   * returned).
   *
   * @param other the timespan to subtract from this one
   * @return a new timespan representing {@code this - other}
   */
  public Timespan substractWithZeroFloor(Timespan other)
  {
    if (getTimeUnit() == other.getTimeUnit())
    {
      long delta = Math.max(0, getDuration() - other.getDuration());
      return new Timespan(delta, getTimeUnit());
    }

    long delta = Math.max(0, getDurationInMilliseconds() - other.getDurationInMilliseconds());

    return new Timespan(delta, TimeUnit.MILLISECOND);
  }

  /**
   * @return the duration of this timespan in milliseconds
   */
  public long getDurationInMilliseconds()
  {
    // 100% equivalent to getDuration(TimeUnit.MILLISECOND) but faster!
    return getDuration() * getTimeUnit().getMillisecondsCount();
  }

  /**
   * @param timeUnit the unit of time you want this timespan ass
   * @return the duration of this timespan expressed in the time unit provided. Note that all
   * units below timeUnit will be truncated! (ex: 3h20m45s will return 3 if timeUnit=HOUR).
   */
  public long getDuration(TimeUnit timeUnit)
  {
    return truncate(timeUnit).getDuration();
  }

  /**
   * @return the duration of this timespan expressed in seconds. If milliseconds are present,
   * they will be truncated!
   */
  public long getDurationInSeconds()
  {
    return getDuration(TimeUnit.SECOND);
  }

  /**
   * @return the duration of this timespan expressed in minutes. If s/ms are present,
   * they will be truncated!
   */
  public long getDurationInMinutes()
  {
    return getDuration(TimeUnit.MINUTE);
  }

  /**
   * @return the duration of this timespan expressed in hours. If m/s/ms are present,
   * they will be truncated!
   */
  public long getDurationInHours()
  {
    return getDuration(TimeUnit.HOUR);
  }

  /**
   * @return a (potentially new) version of this timestamp where the unit is
   * {@link TimeUnit#MILLISECOND}.
   */
  public Timespan toMillisecondsTimespan()
  {
    if(getTimeUnit() == TimeUnit.MILLISECOND)
      return this;

    return new Timespan(getDurationInMilliseconds(), TimeUnit.MILLISECOND);
  }

  /**
   * Truncates this timespan to the given time unit. Example: if this is 1h20m5s then
   * truncate(TimeUnit.HOUR) will return 1h,
   * truncate(TimeUnit.MINUTE) will return 1h20m and
   * truncate(TimeUnit.SECOND) will return 1h20m5s
   *
   * @param timeUnit the unit you want the timespan in
   * @return this timespan if time unit matches otherwise a new one with the given unit
   */
  public Timespan truncate(TimeUnit timeUnit)
  {
    if(getTimeUnit() == timeUnit)
      return this;

    return truncateDurationToUnit(getDurationInMilliseconds(), timeUnit);
  }

  /**
   * There are many ways to represent the same timespan: 3h, 180m... this call return the unique
   * way to express it such that ms is < 1000, s is < 60, m is < 60, h is < 24,
   * d is < 7.
   * @return this timespan as a canonical representation: an entry for w/dh/m/s/ms is returned
   * @see #getAsTimespans(EnumSet)
   */
  public EnumMap<TimeUnit, Timespan> getCanonicalTimespans()
  {
    return getAsTimespans(CANONICAL_TIME_UNITS);
  }

  /**
   * @return a string representing this timespan (ex: 3h2m23s). Note that if a duration is missing,
   * it is not part of the string (the string would be 3h23s and not 3h0m23s).
   * @see #getCanonicalTimespans()
   */
  public String getCanonicalString()
  {
    return getAsString(CANONICAL_TIME_UNITS);
  }

  /**
   * Decomposes this timespan as a map for each unit provided. Example: if this timespan represents
   * 63s and you provide m/s/ms then you will get 3 timespans in the map: one of 1mn, one for 3s
   * and one for 0ms. (Convenient call).
   *
   * @param timeUnits the time units you want to be part of the decomposition.
   * @return a map containing an entry for each time unit provided. All others are null.
   * @see #getAsTimespans(EnumSet)
   */
  public EnumMap<TimeUnit, Timespan> getAsTimespans(TimeUnit... timeUnits)
  {
    return getAsTimespans(CollectionsUtils.toEnumSet(TimeUnit.class, timeUnits));
  }

  /**
   * Decomposes this timespan as a map for each unit provided. Example: if this timespan represents
   * 63s and you provide m/s/ms then you will get 3 timespans in the map: one of 1mn, one for 3s
   * and one for 0ms.
   *
   * @param timeUnits the time units you want to be part of the decomposition.
   * @return a map containing an entry for each time unit provided. All others are null.
   */
  public EnumMap<TimeUnit, Timespan> getAsTimespans(EnumSet<TimeUnit> timeUnits)
  {
    EnumMap<TimeUnit,Timespan> res = new EnumMap<TimeUnit, Timespan>(TimeUnit.class);

    long durationInMillis = getDurationInMilliseconds();

    for(TimeUnit timeUnit : TIME_UNIT_ORDER)
    {
      if(timeUnits.contains(timeUnit))
      {
        Timespan timespan = truncateDurationToUnit(durationInMillis, timeUnit);
        res.put(timeUnit, timespan);
        durationInMillis -= timespan.getDurationInMilliseconds();

      }
    }

    return res;
  }

  /**
   * Filters this timespan with only the unit provided. It computes the canonical representation
   * and keeps only the units that are present in the filter. Example: if this timespan represents
   * 3d2h25m10s and filter is d/h/m then result is 3d2h25m. Convenient call.
   *
   * @param timeUnits the time units you want to keep in the filtering.
   * @return the timespan after the filtering
   * @see #filter(EnumSet)
   */
  public Timespan filter(TimeUnit... timeUnits)
  {
    return filter(CollectionsUtils.toEnumSet(TimeUnit.class, timeUnits));
  }

  /**
   * Filters this timespan with only the unit provided. It computes the canonical representation
   * and keeps only the units that are present in the filter. Example: if this timespan represents
   * 3d2h25m10s and filter is d/h/m then result is 3d2h25m.
   *
   * @param timeUnits the time units you want to keep in the filtering.
   * @return the timespan after the filtering
   */
  public Timespan filter(EnumSet<TimeUnit> timeUnits)
  {
    Timespan res = null;

    EnumMap<TimeUnit,Timespan> canonicalTimespans = getCanonicalTimespans();

    for(TimeUnit timeUnit : TIME_UNIT_ORDER)
    {
      if(timeUnits.contains(timeUnit))
      {
        Timespan timespan = canonicalTimespans.get(timeUnit);
        if(timespan != null && timespan.getDuration() > 0)
        {
          if(res == null)
          {
            res = timespan;
          }
          else
          {
            res = res.add(timespan);
          }
        }
      }
    }

    // in case there is no match we return with smallest timeunit provided
    if(res == null)
      res = ZERO_TIMESPANS.get(timeUnits.iterator().next());

    return res;
  }

  /**
   * Returns a string representing this timespan expressed with the units provided.
   * (convenient call)
   *
   * @param timeUnits the timeunits you want in the decomposition
   * @return a string representation using the units.
   * @see #getAsString(EnumSet)
   */
  public String getAsString(TimeUnit... timeUnits)
  {
    return getAsString(CollectionsUtils.toEnumSet(TimeUnit.class, timeUnits));
  }

  /**
   * Returns a string representing this timespan expressed with the units provided.
   *
   * @param timeUnits the timeunits you want in the decomposition
   * @return a string representation using the units.
   * @see #getAsTimespans(EnumSet)
   */
  public String getAsString(EnumSet<TimeUnit> timeUnits)
  {
    StringBuilder sb = new StringBuilder();

    EnumMap<TimeUnit, Timespan> canonicalTimespans = getAsTimespans(timeUnits);
    for(TimeUnit timeUnit : TIME_UNIT_ORDER)
    {
      if(canonicalTimespans.containsKey(timeUnit))
      {
        long duration = canonicalTimespans.get(timeUnit).getDuration();
        if(duration > 0)
        {
          sb.append(duration).append(timeUnit.getDisplayChar());
        }
      }
    }

    if(sb.length() == 0)
    {
      sb.append(0);
      if(timeUnits.contains(getTimeUnit()))
        sb.append(getTimeUnit().getDisplayChar());
    }

    return sb.toString();
  }

  /**
   * 2 timespans can be different (1h and 3600s) while representing the same duration expressed
   * in milliseconds... This method tests for this.
   *
   * @param timespan the other timespan to compare with
   * @return true if this timespan and the provided one represent the same
   * duration of time.
   */
  public boolean equalsDurationInMilliseconds(Timespan timespan)
  {
    // shortcut when time unit are the same
    if(timespan.getTimeUnit() == getTimeUnit())
      return timespan.getDuration() == getDuration();
    else
      return getDurationInMilliseconds() == timespan.getDurationInMilliseconds();
  }

  /**
   * @param clock the clock to extract 'now'
   * @return the absolute time in milliseconds (since jan 01 1970) represented by now + this
   * timespan
   */
  public long futureTimeMillis(Clock clock)
  {
    return futureTimeMillis(clock.currentTimeMillis());
  }

  /**
   * @param baseMilliseconds the starting point to compute the future time
   * @return the absolute time in milliseconds (since jan 01 1970) represented
   * {@code baseMilliseconds} + this timespan
   */
  public long futureTimeMillis(long baseMilliseconds)
  {
    return baseMilliseconds + getDurationInMilliseconds();
  }

  /**
   * @param clock the clock to extract 'now'
   * @return the absolute time in milliseconds (since jan 01 1970) represented by now - this
   * timespan
   */
  public long pastTimeMillis(Clock clock)
  {
    return clock.currentTimeMillis() - getDurationInMilliseconds();
  }

  /**
   * @param baseMillis the starting point
   * @return baseMillis - this timespan
   */
  public long pastTimeMillis(long baseMillis)
  {
    return baseMillis - getDurationInMilliseconds();
  }

  /**
   * @param clock the clock to extract 'now'
   * @return the date representing a time in milliseconds (since jan 01 1970) represented by now + this
   * timespan
   */
  public Date futureDate(Clock clock)
  {
    return new Date(futureTimeMillis(clock));
  }

  /**
   * @param baseDate base date to offset from
   * @return return the date which is baseDate + value of this timespan
   */
  public Date futureDate(Date baseDate)
  {
    return new Date(baseDate.getTime() + getDurationInMilliseconds());
  }

  /**
   *
   * @param baseDate base date to offset from
   * @return return the date which is baseDate - value of this timespan
   */
  public Date pastDate(Date baseDate)
  {
    return new Date(pastTimeMillis(baseDate.getTime()));
  }

  /**
   * @param clock the clock to extract 'now'
   * @return the date representing a time in milliseconds (since jan 01 1970) represented by now - this
   * timespan
   */
  public Date pastDateMillis(Clock clock)
  {
    return new Date(pastTimeMillis(clock));
  }

  /**
   * Compares 2 timespan. Based on duration in milliseconds.
   *
   * @param timespan
   * @return 0 if same duration in milliseconds,
   */
  @Override
  public int compareTo(Timespan timespan)
  {
    if(timespan.getTimeUnit() == getTimeUnit())
      return LangUtils.compare(getDuration(), timespan.getDuration());
    else
      return LangUtils.compare(getDurationInMilliseconds(), timespan.getDurationInMilliseconds());
  }

  /**
   * Expresses the provided duration in the unit provided. Note that the timespan returned
   * represent only the truncated version of the duration: if duration is 1002ms and timeunit
   * is seconds, then the timespan returned is 1 second... leaving behind 2ms.
   *
   * @param durationInMillis
   * @param timeUnit
   * @return the timespan
   */
  private static Timespan truncateDurationToUnit(long durationInMillis, TimeUnit timeUnit)
  {
    Timespan res;

    if(durationInMillis >= timeUnit.getMillisecondsCount())
    {
      res = new Timespan(durationInMillis / timeUnit.getMillisecondsCount(),
                         timeUnit);
    }
    else
    {
      res = ZERO_TIMESPANS.get(timeUnit);
    }

    return res;
  }

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

    final Timespan timespan = (Timespan) o;

    return compareTo(timespan) == 0;
  }

  @Override
  public int hashCode()
  {
    long duration = getDurationInMilliseconds();
    return (int) (duration ^ (duration >>> 32));
  }

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

  /**
   * Creates a timespan from a list of other timespans.
   *
   * @param timespans
   * @return a timespan representing the sum of all the timespans provided
   */
  public static Timespan create(Timespan... timespans)
  {
    if(timespans == null)
      return null;

    if(timespans.length == 0)
      return ZERO_MILLISECONDS;

    Timespan res = timespans[0];

    for(int i = 1; i < timespans.length; i++)
    {
      Timespan timespan = timespans[i];
      res = res.add(timespan);
    }

    return res;
  }

  /**
   * Synonym.
   *
   * @see #parseTimespan(String)
   */
  public static Timespan parse(String timespan)
  {
    return parseTimespan(timespan);
  }

  /**
   * Synonym.
   *
   * @see #parseTimespan(String)
   */
  public static Timespan valueOf(String timespan)
  {
    return parseTimespan(timespan);
  }

  /**
   * Convenient call when providing milliseconds
   *
   * @param milliseconds
   * @return the timespan
   */
  public static Timespan milliseconds(long milliseconds)
  {
    return new Timespan(milliseconds);
  }

  /**
   * Convenient call when providing seconds
   *
   * @param seconds
   * @return the timespan
   */
  public static Timespan seconds(long seconds)
  {
    return new Timespan(seconds, TimeUnit.SECOND);
  }

  /**
   * Convenient call when providing minutes
   *
   * @param minutes
   * @return the timespan
   */
  public static Timespan minutes(long minutes)
  {
    return new Timespan(minutes, TimeUnit.MINUTE);
  }

  /**
   * Parses the provided string as a timespan. It should follow the pattern returned by
   * {@link #getCanonicalString()}. Example: 10m30s
   *
   * @param timespan
   * @return the timespan to parse
   * @throws IllegalArgumentException if the string is not valid
   */
  public static Timespan parseTimespan(String timespan)
  {
    if(timespan == null)
      return null;

    int len = timespan.length();
    if(len == 0)
      return ZERO_MILLISECONDS;

    int count = 0;
    int timeUnitOrderIdx = 0;
    int timeUnitOrderLen = TIME_UNIT_ORDER.length;
    Timespan[] timespans = new Timespan[timeUnitOrderLen];

    int startDigitsIdx = 0;
    boolean expectingDigits = true;

    for(int i = 0; i < len; i++)
    {
      char c = timespan.charAt(i);
      if(c >= '0' && c <= '9')
      {
        expectingDigits = false;
        continue;
      }

      if(expectingDigits)
        throw new IllegalArgumentException("found " + c + " was expecting a digit");

      for(; timeUnitOrderIdx < timeUnitOrderLen; timeUnitOrderIdx++)
      {
        TimeUnit timeUnit = TIME_UNIT_ORDER[timeUnitOrderIdx];
        String displayChar = timeUnit.getDisplayChar();
        if(displayChar.length() == 0)
          throw new IllegalArgumentException("found nothing was expecting: " + c);
        if(c == displayChar.charAt(0))
        {
          try
          {
            long duration = Long.parseLong(timespan.substring(startDigitsIdx, i));
            timespans[timeUnitOrderIdx++] = new Timespan(duration, timeUnit);
            startDigitsIdx = i + 1;
            expectingDigits = true;
            count++;
            break;
          }
          catch(NumberFormatException e)
          {
            throw new IllegalArgumentException(e);
          }
        }
      }
    }

    if(startDigitsIdx < len)
    {
      try
      {
        long duration = Long.parseLong(timespan.substring(startDigitsIdx, len));
        timespans[timeUnitOrderLen - 1] = new Timespan(duration, TimeUnit.MILLISECOND);
        count++;
      }
      catch(NumberFormatException e)
      {
        throw new IllegalArgumentException(e);
      }
    }

    Timespan[] ts = new Timespan[count];
    for(int i = 0, idx = 0; i < timespans.length; i++)
    {
      Timespan t = timespans[i];
      if(t != null)
        ts[idx++] = t;
    }
    return create(ts);
  }

  /**
   * Shortcut for creating a timespan and then retrieving the value in milliseconds.
   *
   * @param timespan the timespan as a string
   * @return the value in milliseconds
   */
  public static long toMilliseconds(String timespan)
  {
    return parseTimespan(timespan).getDurationInMilliseconds();
  }
}


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

package org.linkedin.util.lang;

import org.linkedin.util.collections.CollectionsUtils;

import java.io.Serializable;
import java.util.EnumMap;
import java.util.EnumSet;

/**
 * This class represents memory size in terms of size and unit.
 * The size units supported range from bytes to tera bytes. Size
 * can be positive or negative. Instances of this class are immutable
 * and thread safe
 *
 * @author qsu
 *
 */
public class MemorySize implements Comparable<MemorySize>, Serializable
{
  private static final long serialVersionUID = 1L;
  public enum SizeUnit
  {
    // IMPORTANT: the order of enums below needs to be preserved
    // as from smallest to largest unit
    BYTE(1L, ""),
    KILO_BYTE(1024L * BYTE.getBytesCount(), "k"),
    MEGA_BYTE(1024L * KILO_BYTE.getBytesCount(), "m"),
    GIGA_BYTE(1024L * MEGA_BYTE.getBytesCount(), "g"),
    TERA_BYTE(1024L * GIGA_BYTE.getBytesCount(), "t");

    private final long   _bytesCount;
    private final String _displayChar;

    /**
     * Private constructor to ensure singleton for each enum type
     *
     * @param bytesCount
     * @param displayChar
     */
    private SizeUnit(long bytesCount, String displayChar)
    {
      _bytesCount = bytesCount;
      _displayChar = displayChar;
    }

    public long getBytesCount()
    {
      return _bytesCount;
    }

    public String getDisplayChar()
    {
      return _displayChar;
    }
  }

  public final static MemorySize                     ZERO_BYTES      = new MemorySize(0,
                                                                                SizeUnit.BYTE);
  public final static MemorySize                     ZERO_KILO_BYTES = new MemorySize(0,
                                                                                SizeUnit.KILO_BYTE);
  public final static MemorySize                     ZERO_MEGA_BYTES = new MemorySize(0,
                                                                                SizeUnit.MEGA_BYTE);
  public final static MemorySize                     ZERO_GIGA_BYTES = new MemorySize(0,
                                                                                SizeUnit.GIGA_BYTE);
  public final static MemorySize                     ZERO_TERA_BYTES = new MemorySize(0,
                                                                                SizeUnit.TERA_BYTE);

  /** a complete list of size units ordered descendingly */
  private final static SizeUnit[]                    ORDERED_SIZE_UNIT = CollectionsUtils.reverse(SizeUnit.values());

  private final static EnumMap<SizeUnit, MemorySize> ZERO_SIZES      = new EnumMap<SizeUnit, MemorySize>(SizeUnit.class);

  static
  {
    ZERO_SIZES.put(SizeUnit.BYTE, ZERO_BYTES);
    ZERO_SIZES.put(SizeUnit.KILO_BYTE, ZERO_KILO_BYTES);
    ZERO_SIZES.put(SizeUnit.MEGA_BYTE, ZERO_MEGA_BYTES);
    ZERO_SIZES.put(SizeUnit.GIGA_BYTE, ZERO_GIGA_BYTES);
    ZERO_SIZES.put(SizeUnit.TERA_BYTE, ZERO_TERA_BYTES);
  }

  private final long _size;
  private final SizeUnit _sizeUnit;

  /**
   * Constructor
   *
   * @param sizeInBytes
   */
  public MemorySize(long sizeInBytes)
  {
    this(sizeInBytes, SizeUnit.BYTE);
  }

  /**
   * Constructor
   *
   * @param size
   * @param sizeUnit
   */
  public MemorySize(long size, SizeUnit sizeUnit)
  {
    _size = size;
    _sizeUnit = sizeUnit;
  }

  public long getSize()
  {
    return _size;
  }

  public SizeUnit getSizeUnit()
  {
    return _sizeUnit;
  }

  /**
   * @return size in bytes
   */
  public long getSizeInBytes()
  {
    return getSize() * getSizeUnit().getBytesCount();
  }

  /**
   * @return size in kilo bytes, the remaining size if any istruncated
   */
  public long getSizeInKiloBytes()
  {
    return getSizeInBytes() / SizeUnit.KILO_BYTE.getBytesCount();
  }

  /**
   * @return size in mega bytes, the remaining size if any istruncated
   */
  public long getSizeInMegaBytes()
  {
    return getSizeInBytes() / SizeUnit.MEGA_BYTE.getBytesCount();
  }

  /**
   * @return size in giga bytes, the remaining size if any istruncated
   */
  public long getSizeInGigaBytes()
  {
    return getSizeInBytes() / SizeUnit.GIGA_BYTE.getBytesCount();
  }

  /**
   * @return size in tera bytes, the remaining size if any istruncated
   */
  public long getSizeInTeraBytes()
  {
    return getSizeInBytes() / SizeUnit.TERA_BYTE.getBytesCount();
  }

  /**
   * Return size in the given unit to floating point precision, useful when
   * truncating behavior is not desired
   *
   * @param sizeUnit
   * @return size in the given unit to floating point precision
   */
  public double getFractionalSize(SizeUnit sizeUnit)
  {
    return (double) getSizeInBytes() / sizeUnit.getBytesCount();
  }

  /**
   * Returns this memory size as a fractional size representation using the biggest unit size
   * possible. ex: 1.23g
   *
   * @return a string representing a this memory size as a fraction using the biggest size unit
   * possible
   */
  public String getFractionalSizeAsString()
  {
    if(_size == 0)
      return "0";

    long sizeInBytes = getSizeInBytes();

    // determine the biggest size unit with non 0 size
    for(SizeUnit sizeUnit : ORDERED_SIZE_UNIT)
    {
      if(sizeUnit == SizeUnit.BYTE)
        return String.valueOf(sizeInBytes);

      if(sizeInBytes >= sizeUnit.getBytesCount())
      {
        double fractionalSize = (double) sizeInBytes / sizeUnit.getBytesCount();
        return String.format("%.2f%s", fractionalSize, sizeUnit.getDisplayChar());
      }
    }

    throw new RuntimeException("should not reach this line...");
  }

  /**
   * Return a new instance of MemorySize that is truncated to the given unit
   *
   * @param sizeUnit
   * @return a new instance of MemorySize that is truncated to the given unit
   */
  public MemorySize truncate(SizeUnit sizeUnit)
  {
    if (getSizeUnit() == sizeUnit)
      return this;

    long sizeInBytes = getSizeInBytes();

    if (sizeInBytes >= sizeUnit.getBytesCount())
    {
      return new MemorySize(sizeInBytes / sizeUnit.getBytesCount(), sizeUnit);
    }
    else
    {
      return ZERO_SIZES.get(sizeUnit);
    }
  }

  /**
   * Return a new instance of MemorySize that adds up the size of this object
   * with that of the 'other'
   *
   * @param other
   * @return a new instance of MemorySize that adds up the size of this object
   * with the that of the 'other'
   */
  public MemorySize add(MemorySize other)
  {
    if(other == null)
      throw new NullPointerException();

    if(getSizeUnit() == other.getSizeUnit())
      return new MemorySize(getSize() + other.getSize(), getSizeUnit());

    return new MemorySize(getSizeInBytes() + other.getSizeInBytes(),
                        SizeUnit.BYTE);
  }

  /**
   * Return a new instance of MemorySize that subtract from this object
   * the size of the other object
   *
   * @param other
   * @return a new instance of MemorySize that subtract from this object
   * the size of the other object
   */
  public MemorySize subtract(MemorySize other)
  {
    if(other == null)
      throw new NullPointerException();

    if(getSizeUnit() == other.getSizeUnit())
      return new MemorySize(getSize() - other.getSize(), getSizeUnit());

    return new MemorySize(getSizeInBytes() - other.getSizeInBytes(),
                        SizeUnit.BYTE);
  }

  /**
   * Return a string representation of this instance using the given units
   * E.g. 2g3m4k given units g/m -> "2g3m"
   *      2g3m4k given units g/k -> "2g3076k"
   *        3m4k given units t/g -> "0g"
   *        2g3m given units k/b -> "2100224k"
   *
   * @param sizeUnits the sizeUnits you want in the decomposition
   * @return a string representation of this instance using the given units.
   */
  public String getAsString(SizeUnit...sizeUnits)
  {
    if(sizeUnits == null || sizeUnits.length == 0)
      return toString();

    return getAsString(CollectionsUtils.toEnumSet(SizeUnit.class, sizeUnits));
  }

  /**
   * Return a string representation of this instance using the given units
   * E.g. 2g3m4k given units g/m -> "2g3m"
   *      2g3m4k given units g/k -> "2g3076k"
   *        3m4k given units t/g -> "0g"
   *        2g3m given units k/b -> "2100224k"
   *
   * @param sizeUnits the sizeUnits you want in the decomposition
   * @return a string representation of this instance using the given units.
   */
  public String getAsString(EnumSet<SizeUnit> sizeUnits)
  {
    if(sizeUnits == null || sizeUnits.size() == 0)
      return toString();

    StringBuilder sb = new StringBuilder();
    MemorySize ms = this;
    MemorySize prevMs;

    // IMPORTANT:
    // according to javadoc for EnumSet:
    // "The iterator returned by the iteratormethod traverses the elements in their natural order
    // (the order in which the enum constants are declared)" and the toArray() method of AbstractCollection
    // guarantees such an order
    Object[] unitsAsArr = sizeUnits.toArray();

    int len = unitsAsArr.length;
    for (int i = len - 1; i > -1; i--)
    {
      prevMs = ms;
      ms = ms.truncate((SizeUnit)unitsAsArr[i]);
      if (ms.getSize() > 0)
      {
        sb.append(ms.getSize()).append(ms.getSizeUnit().getDisplayChar());
      }
      ms = prevMs.subtract(ms);
    }

    if(sb.length() == 0)
    {
      SizeUnit smallestUnit = (SizeUnit)unitsAsArr[0];
      sb.append("0").append(smallestUnit.getDisplayChar());
    }

    return sb.toString();
  }

  /**
   * Return a canonical representation of size where:
   * byte is < 1024, kilo byte is < 1024, mega byte is < 1024,
   * giga byte is < 1024, tera byte is < 1024.
   *
   * Unless it is representing the smallest unit, 0 size of a unit is not displayed
   *
   * @return canonical representation of size
   */
  public String getCanonicalString()
  {
    return getAsString(ORDERED_SIZE_UNIT);
  }

  /**
   * Return a canonical representation of size
   * see {@link #getCanonicalString()}
   *
   * @return canonical representation of size
   */
  @Override
  public String toString()
  {
    return getFractionalSizeAsString();
  }

  /**
   * Compare with another instance of MemorySize based on size in bytes
   *
   * @see java.lang.Comparable#compareTo(java.lang.Object)
   */
  @Override
  public int compareTo(MemorySize memorySize)
  {
    // according to javadoc for interface Comparable<T>, a NullPointerException
    // should be thrown when the object to be compared with is null
    // http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Comparable.html#compareTo(T)
    if (memorySize == null)
      throw new NullPointerException();

    if (getSizeUnit() == memorySize.getSizeUnit())
      return LangUtils.compare(getSize(), memorySize.getSize());

    return LangUtils.compare(getSizeInBytes(), memorySize.getSizeInBytes());
  }

  /**
   * Override java.lang.Object#equals(java.lang.Object)
   *
   * @return true if o is of class MemorySize and their size in bytes
   * are the same. Consistent with compareTo()
   */
  @Override
  public boolean equals(Object o)
  {
    if (this == o)
      return true;
    if (o == null || getClass() != o.getClass())
      return false;

    return compareTo((MemorySize) o) == 0;
  }

  /**
   * Override java.lang.Object#hashCode()
   *
   * @return hashCode of this object based on its size in byte
   */
  @Override
  public int hashCode()
  {
    long size = getSizeInBytes();
    return (int) (size ^ (size >>> 32));
  }

  /**
   * Create a new instance of MemorySize by adding up the given
   * array of memorySizes
   *
   * @param memorySizes
   * @return a new instance of MemorySize
   */
  public static MemorySize create(MemorySize...memorySizes)
  {
    if(memorySizes == null)
      return null;

    if(memorySizes.length == 0)
      return ZERO_BYTES;

    MemorySize res = memorySizes[0];

    for(int i = 1; i < memorySizes.length; i++)
    {
      MemorySize memorySize = memorySizes[i];
      if(memorySize != null)
        res = res.add(memorySize);
    }

    return res;
  }

  /**
   * Synonym...
   *
   * @param memorySizeAsStr
   * @return a new instance of MemorySize
   */
  public static MemorySize parse(String memorySizeAsStr)
  {
    return parseMemorySize(memorySizeAsStr);
  }

  /**
   * Create a new instance of MemorySize by parsing the given string
   * of format e.g. 2g3m4k5
   *
   * @param memorySizeAsStr
   * @return a new instance of MemorySize
   */
  public static MemorySize parseMemorySize(String memorySizeAsStr)
  {
    if(memorySizeAsStr == null || memorySizeAsStr.length() == 0)
      return null;

    int len = memorySizeAsStr.length();

    int orderedSizeUnitIdx = 0;
    int orderedSizeUnitLen = ORDERED_SIZE_UNIT.length;

    int startDigitsIdx = 0;
    boolean expectingDigits = true;

    MemorySize ms = ZERO_BYTES;
    for(int i = 0; i < len; i++)
    {
      char c = memorySizeAsStr.charAt(i);
      if(c >= '0' && c <= '9')
      {
        expectingDigits = false;
        continue;
      }

      if(expectingDigits)
        throw new IllegalArgumentException("Unable to parse '" + memorySizeAsStr + "': found '" + c
            + "' at pos " + i + ", was expecting a digit");

      for(; orderedSizeUnitIdx < orderedSizeUnitLen; orderedSizeUnitIdx++)
      {
        SizeUnit sizeUnit = ORDERED_SIZE_UNIT[orderedSizeUnitIdx];
        String displayChar = sizeUnit.getDisplayChar();

        if(displayChar.length() > 0 && c == displayChar.charAt(0))
        {
          try
          {
            long size = Long.parseLong(memorySizeAsStr.substring(startDigitsIdx, i));
            ms = ms.add(new MemorySize(size, sizeUnit));
            orderedSizeUnitIdx++;
            startDigitsIdx = i + 1;
            expectingDigits = true;
            break;
          }
          catch(NumberFormatException e)
          {
            throw new IllegalArgumentException("Unable to parse '" + memorySizeAsStr + "'", e);
          }
        }
      }

      if(orderedSizeUnitIdx == orderedSizeUnitLen)
        throw new IllegalArgumentException("Unable to parse '" + memorySizeAsStr
            + "': found invalid character '" + c + "' at pos " + i);
    }

    if(startDigitsIdx < len)
    {
      try
      {
        long size = Long.parseLong(memorySizeAsStr.substring(startDigitsIdx, len));
        ms = ms.add(new MemorySize(size, SizeUnit.BYTE));
      }
      catch(NumberFormatException e)
      {
        throw new IllegalArgumentException("Unable to parse '" + memorySizeAsStr + "'", e);
      }
    }
    return ms;
  }
}


/*
 * 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 java.util.concurrent.Executor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * This executor simply delegates to the thread factory to create a new thread everytime.
 *
 * @author ypujante@linkedin.com
 *
 */
public class ThreadPerTaskExecutor implements Executor
{
  private final ThreadFactory _threadFactory;

  /**
   * Constructor
   */
  public ThreadPerTaskExecutor()
  {
    this(new ThreadFactory()
    {
      @Override
      public Thread newThread(Runnable r)
      {
        return new Thread(r);
      }
    });
  }

  /**
   * Constructor
   */
  public ThreadPerTaskExecutor(ThreadFactory threadFactory)
  {
    _threadFactory = threadFactory;
  }

  @Override
  public void execute(Runnable task)
  {
    Thread thread = _threadFactory.newThread(task);
    thread.start();
  }

  /**
   * Executes the callable in a separate thread and return the future to get the result. Note
   * that this implementation is not efficient and should be used very carefully.
   *
   * @param callable
   * @return the future to get the result.
   */
  public static <V> Future<V> execute(Callable<V> callable)
  {
    FutureTask<V> futureTask = new FutureTask<V>(callable);

    new Thread(futureTask).start();

    return futureTask;
  }
}


/*
 * Copyright (C) 2015 Oguz Babaoglu
 *
 * 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 com.oguzbabaoglu.fancymarkers.examples;

import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBarActivity;
import android.view.View;

import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.oguzbabaoglu.fancymarkers.CustomMarker;
import com.oguzbabaoglu.fancymarkers.IconMarker;
import com.oguzbabaoglu.fancymarkers.MarkerManager;

import java.util.ArrayList;

/**
 * @author Oguz Babaoglu
 */
public class ExampleActivity extends ActionBarActivity implements View.OnClickListener {

    private static final LatLng CENTER = new LatLng(36.778261, -119.417932);
    private static final int ZOOM = 6;

    private static final LatLng[] LOCATIONS = new LatLng[]{
            new LatLng(37.71515248, -120.7508706),
            new LatLng(36.37386931, -118.47269228),
            new LatLng(36.84395714, -121.66135364),
            new LatLng(37.44175323, -119.08338699),
            new LatLng(35.36812138, -120.75986352)
    };

    private static ImageLoader imageLoader;

    private MarkerManager<IconMarker> iconMarkerManager;
    private MarkerManager<ColorMarker> colorMarkerManager;
    private MarkerManager<NetworkMarker> networkMarkerManager;

    private GoogleMap map;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_example);

        findViewById(R.id.button_icon_markers).setOnClickListener(this);
        findViewById(R.id.button_color_markers).setOnClickListener(this);
        findViewById(R.id.button_network_markers).setOnClickListener(this);

        final SupportMapFragment mapFragment = SupportMapFragment.newInstance();

        final FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.add(R.id.map_container, mapFragment);
        transaction.commit();

        mapFragment.getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(GoogleMap googleMap) {
                iconMarkerManager = new MarkerManager<>(googleMap);
                colorMarkerManager = new MarkerManager<>(googleMap);
                networkMarkerManager = new MarkerManager<>(googleMap);

                iconMarkerManager.setOnMarkerClickListener(new DisableClick<IconMarker>());
                colorMarkerManager.setOnMarkerClickListener(new DisableClick<ColorMarker>());
                networkMarkerManager.setOnMarkerClickListener(new DisableClick<NetworkMarker>());

                map = googleMap;
                map.moveCamera(CameraUpdateFactory.newLatLngZoom(CENTER, ZOOM));
            }
        });

        // Keep a static image loader instance.
        if (imageLoader == null) {

            final RequestQueue queue = Volley.newRequestQueue(this);
            imageLoader = new ImageLoader(queue, new NoImageCache());
        }
    }

    @Override
    public void onClick(View v) {

        iconMarkerManager.clear();
        colorMarkerManager.clear();
        networkMarkerManager.clear();

        switch (v.getId()) {

            case R.id.button_icon_markers:
                map.setOnMarkerClickListener(iconMarkerManager);
                iconMarkerManager.addMarkers(createIconMarkers());
                break;

            case R.id.button_color_markers:
                map.setOnMarkerClickListener(colorMarkerManager);
                colorMarkerManager.addMarkers(createColorMarkers());
                break;

            case R.id.button_network_markers:
                map.setOnMarkerClickListener(networkMarkerManager);
                networkMarkerManager.addMarkers(createNetworkMarkers());
                break;
        }
    }

    /**
     * Icons courtesy of https://mapicons.mapsmarker.com/
     */
    private ArrayList<IconMarker> createIconMarkers() {

        final ArrayList<IconMarker> iconMarkers = new ArrayList<>(LOCATIONS.length);

        iconMarkers.add(new IconMarker(LOCATIONS[0], R.drawable.bread, R.drawable.bread_c));
        iconMarkers.add(new IconMarker(LOCATIONS[1], R.drawable.butcher, R.drawable.butcher_c));
        iconMarkers.add(new IconMarker(LOCATIONS[2], R.drawable.fruits, R.drawable.fruits_c));
        iconMarkers.add(new IconMarker(LOCATIONS[3], R.drawable.grocery, R.drawable.grocery_c));
        iconMarkers.add(new IconMarker(LOCATIONS[4], R.drawable.patisserie, R.drawable.patisserie_c));

        return iconMarkers;
    }

    private ArrayList<ColorMarker> createColorMarkers() {

        final ArrayList<ColorMarker> colorMarkers = new ArrayList<>(LOCATIONS.length);

        for (LatLng location : LOCATIONS) {
            colorMarkers.add(new ColorMarker(this, location));
        }

        return colorMarkers;
    }

    private ArrayList<NetworkMarker> createNetworkMarkers() {

        final ArrayList<NetworkMarker> networkMarkers = new ArrayList<>(LOCATIONS.length);

        for (LatLng location : LOCATIONS) {
            networkMarkers.add(new NetworkMarker(this, location, imageLoader));
        }

        return networkMarkers;
    }

    /**
     * Not interested in caching images.
     */
    private static class NoImageCache implements ImageLoader.ImageCache {

        @Override
        public Bitmap getBitmap(String url) {
            return null;
        }

        @Override
        public void putBitmap(String url, Bitmap bitmap) {
            // Do nothing
        }
    }

    /**
     * Marker click listener that always returns true.
     */
    private static class DisableClick<T extends CustomMarker>
            implements MarkerManager.OnMarkerClickListener<T> {

        @Override
        public boolean onMarkerClick(T marker) {
            return true;
        }
    }

}


/*
 * Copyright (C) 2015 Oguz Babaoglu
 *
 * 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 com.oguzbabaoglu.fancymarkers;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * All addition and removal of markers needs to be done through this class.
 *
 * @author Oguz Babaoglu
 */
public class MarkerManager<T extends CustomMarker> implements
        GoogleMap.OnMarkerClickListener,
        GoogleMap.OnInfoWindowClickListener {

    private final GoogleMap googleMap;

    private OnMarkerClickListener<T> onMarkerClickListener;
    private OnInfoWindowClickListener<T> onInfoWindowClickListener;

    private T lastSelected;

    @Override
    public void onInfoWindowClick(Marker marker) {
        if (onInfoWindowClickListener != null) {
            onInfoWindowClickListener.onInfoWindowClick(markerCache.get(marker));
        }
    }

    @Override
    public boolean onMarkerClick(Marker marker) {

        if (lastSelected != null) {
            lastSelected.setSelected(false);
        }

        final T item = markerCache.get(marker);

        if (item != null) {
            item.setSelected(true);
            lastSelected = item;
        }

        return onMarkerClickListener != null
                && onMarkerClickListener.onMarkerClick(markerCache.get(marker));
    }

    public interface OnMarkerClickListener<T extends CustomMarker> {

        boolean onMarkerClick(T marker);
    }

    public interface OnInfoWindowClickListener<T extends CustomMarker> {

        void onInfoWindowClick(T marker);
    }

    public void setOnMarkerClickListener(OnMarkerClickListener<T> onMarkerClickListener) {
        this.onMarkerClickListener = onMarkerClickListener;
    }

    public void setOnInfoWindowClickListener(OnInfoWindowClickListener<T> onInfoWindowClickListener) {
        this.onInfoWindowClickListener = onInfoWindowClickListener;
    }

    /**
     * Marker cache for on screen markers.
     */
    private MarkerCache<T> markerCache = new MarkerCache<>();

    public MarkerManager(GoogleMap map) {
        googleMap = map;
        googleMap.setOnMarkerClickListener(this);
        googleMap.setOnInfoWindowClickListener(this);
    }

    /**
     * A cache of markers.
     */
    private static class MarkerCache<T> {
        private Map<T, Marker> cache = new HashMap<>();
        private Map<Marker, T> cacheReverse = new HashMap<>();

        public Marker get(T item) {
            return cache.get(item);
        }

        public T get(Marker m) {
            return cacheReverse.get(m);
        }

        public void put(T item, Marker m) {
            cache.put(item, m);
            cacheReverse.put(m, item);
        }

        public void clear() {
            cache.clear();
            cacheReverse.clear();
        }

        public void remove(T item) {
            Marker m = cache.get(item);
            cacheReverse.remove(m);
            cache.remove(item);
        }
    }

    /**
     * Add a marker to the map.
     *
     * @param marker marker to add
     */
    public void addMarker(T marker) {

        final MarkerOptions markerOptions = new MarkerOptions();
        marker.setMarkerManager(this);
        marker.prepareMarker(markerOptions);

        markerCache.put(marker, googleMap.addMarker(markerOptions));

        marker.onAdd();
    }

    /**
     * Add all markers to the map.
     *
     * @param markers markers to add
     */
    public void addMarkers(List<T> markers) {

        for (T marker : markers) {
            addMarker(marker);
        }
    }

    /**
     * Remove a marker from the map.
     *
     * @param marker marker to remove
     */
    public void removeMarker(T marker) {

        final Marker realMarker = markerCache.get(marker);

        if (realMarker != null) {
            realMarker.remove();
        }

        markerCache.remove(marker);
    }

    /**
     * Clear the map.
     */
    public void clear() {

        googleMap.clear();
        markerCache.clear();
    }

    /**
     * Get the the real marker object.
     *
     * @param marker marker which to obtain its real marker
     * @return real marker
     */
    public Marker getMarker(T marker) {
        return markerCache.get(marker);
    }
}


/*
 * Copyright (C) 2015 Oguz Babaoglu
 *
 * 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 com.oguzbabaoglu.fancymarkers;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.view.View;

import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;

/**
 * Utility class for creating bitmap icons from views.
 *
 * @author Oguz Babaoglu
 */
public final class BitmapGenerator {

    private BitmapGenerator() {
    }

    /**
     * Returns a bitmap icon showing a screenshot of the view passed in.
     *
     * @param view View to convert
     * @return Bitmap icon of view
     */
    public static BitmapDescriptor fromView(View view) {

        // TODO: Single views have trouble with measure.
        final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        view.measure(spec, spec);

        final int width = view.getMeasuredWidth();
        final int height = view.getMeasuredHeight();

        view.layout(0, 0, width, height);

        final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(bitmap);

        view.draw(canvas);

        return BitmapDescriptorFactory.fromBitmap(bitmap);
    }
}