/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.hadoop.hdfs.server.namenode;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.protocol.Block;
import org.apache.hadoop.hdfs.protocol.DatanodeInfo;
import org.apache.hadoop.hdfs.protocol.LocatedBlock;
import org.apache.hadoop.hdfs.server.namenode.BlockPlacementPolicyDefault;
import org.apache.hadoop.net.DNSToSwitchMapping;
import org.apache.hadoop.net.NetworkTopology;
import org.apache.hadoop.net.Node;
import org.apache.hadoop.raid.RaidNode;
import org.apache.hadoop.raid.Codec;
import org.apache.hadoop.util.HostsFileReader;
import org.apache.hadoop.util.StringUtils;

/**
 * This BlockPlacementPolicy uses a simple heuristic, random placement of
 * the replicas of a newly-created block, for the purpose of spreading out the 
 * group of blocks which used by RAID for recovering each other. 
 * This is important for the availability of the blocks. 
 * 
 * Replication of an existing block continues to use the default placement
 * policy.
 * 
 * This simple block placement policy does not guarantee that
 * blocks on the RAID stripe are on different nodes. However, BlockMonitor
 * will periodically scans the raided files and will fix the placement
 * if it detects violation. 
 * 
 * This class can be used by multiple threads. It has to be thread safe.
 */
public class BlockPlacementPolicyRaid extends BlockPlacementPolicyDefault {
  public static final Log LOG =
    LogFactory.getLog(BlockPlacementPolicyRaid.class);
  Configuration conf;
  private FSNamesystem namesystem = null;

  private CachedLocatedBlocks cachedLocatedBlocks;
  protected CachedFullPathNames cachedFullPathNames;
  private org.apache.hadoop.raid.ConfigManager policyConfig = null;

  /** {@inheritDoc} */
  @Override
  public void initialize(Configuration conf,  FSClusterStats stats,
                         NetworkTopology clusterMap, HostsFileReader hostsReader,
                         DNSToSwitchMapping dnsToSwitchMapping, FSNamesystem namesystem) {
    super.initialize(conf, stats, clusterMap, 
                     hostsReader, dnsToSwitchMapping, namesystem);
    this.conf = conf;
    this.namesystem = namesystem;
	this.cachedLocatedBlocks = new CachedLocatedBlocks(this.conf, namesystem);
    this.cachedFullPathNames = new CachedFullPathNames(this.conf, namesystem);
  }
  
	private void initPolicyConfig() {
		try {
			this.conf.addResource("raid-default.xml");
			this.conf.addResource("raid-site.xml");
			this.policyConfig = new org.apache.hadoop.raid.ConfigManager(
					this.conf);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			LOG.error("Init policyConfig failed: " + e.getMessage());
			e.printStackTrace();
		} 
	}

  @Override
  public DatanodeDescriptor[] chooseTarget(String srcPath, int numOfReplicas,
      DatanodeDescriptor writer, List<DatanodeDescriptor> chosenNodes,
      long blocksize) {
    return chooseTarget(srcPath, numOfReplicas, writer, chosenNodes, null, 
        blocksize);
  }
  
	@Override
	public DatanodeDescriptor[] chooseTarget(String srcPath, int numOfReplicas,
			DatanodeDescriptor writer, List<DatanodeDescriptor> chosenNodes,
			List<Node> exlcNodes, long blocksize) {
		try {
			FileInfo info = getFileInfo(srcPath);
			if (LOG.isDebugEnabled()) {
				LOG.debug("FileType:" + srcPath + " " + info.type.name());
			}
			if (info.type == FileType.NOT_RAID) {
				return super.chooseTarget(srcPath, numOfReplicas, writer,
						chosenNodes, exlcNodes, blocksize);
			}

			ArrayList<DatanodeDescriptor> results = new ArrayList<DatanodeDescriptor>();
			HashMap<Node, Node> excludedNodes = new HashMap<Node, Node>();
			if (exlcNodes != null) {
				for (Node node : exlcNodes) {
					excludedNodes.put(node, node);
				}
			}
			for (Node node : chosenNodes) {
				excludedNodes.put(node, node);
			}

			if (chosenNodes.size() == 0) {
				String src, parity;
				if (info.type == FileType.TO_RAID) {
					src = srcPath;
					parity = null;
				} else if (info.type == FileType.SOURCE) {
					src = srcPath;
					parity = getParityFile(info.codec, src);
				} else  if (info.type == FileType.TEMP_PARITY) {
					String tmpStr = srcPath.substring(info.codec.tmpParityDirectory.length() + 1);
					String str = tmpStr.substring(tmpStr.indexOf("/"));
					src = getSourceFile(str, info.codec.parityDirectory);
					parity = srcPath;
				} else {
					src = getSourceFile(srcPath, info);
					parity = srcPath;
				}

				List<LocatedBlock> srcBlocksList = cachedLocatedBlocks.get(src);
				LocatedBlock[] srcBlocks = srcBlocksList
						.toArray(new LocatedBlock[0]);
				List<LocatedBlock> parityBlocksList = null;
				LocatedBlock[] parityBlocks = null;
				if (parity != null) {
					parityBlocksList = cachedLocatedBlocks.get(parity);
					parityBlocks = parityBlocksList
							.toArray(new LocatedBlock[0]);
				}

				final int parityLength = info.codec.parityLength;
				final int stripeLength = info.codec.stripeLength;

				int stripeIndex;
				if (info.type == FileType.TO_RAID) {
					int blockIndex = srcBlocks.length;
					stripeIndex = blockIndex / stripeLength;
				} else if (info.type == FileType.SOURCE) {
					int blockIndex = srcBlocks.length;
					stripeIndex = blockIndex / stripeLength;
				} else {
					int blockIndex = parityBlocks.length;
					stripeIndex = blockIndex / parityLength;
				} 
				
				int startIndex = stripeIndex * stripeLength;
				int endIndex = Math.min(startIndex + stripeLength, srcBlocks.length);
				for (int i = startIndex; i < endIndex; i++) {
					excludedNodes.put(srcBlocks[i].getLocations()[0],
							srcBlocks[i].getLocations()[0]);
				}
				
				if(parityBlocks != null) {
					startIndex = stripeIndex * parityLength;
					endIndex = Math.min(startIndex + parityLength, parityBlocks.length);
					for (int i = startIndex; i < endIndex; i++) {
						excludedNodes.put(parityBlocks[i].getLocations()[0],
								parityBlocks[i].getLocations()[0]);
					}
				}

			}

			chooseRandom(numOfReplicas, "/", excludedNodes, blocksize, 1,
					results);
			//this.cachedLocatedBlocks.addBlock(srcPath, 
			//		new LocatedBlock(new Block(), new DatanodeDescriptor[results.size()]));
			return results.toArray(new DatanodeDescriptor[results.size()]);
		} catch (Exception e) {
			FSNamesystem.LOG
					.debug("Error happend when choosing datanode to write:"
							+ StringUtils.stringifyException(e));
			return super.chooseTarget(srcPath, numOfReplicas, writer,
					chosenNodes, blocksize);
		}
	}

  /** {@inheritDoc} */
  @Override
  public DatanodeDescriptor chooseReplicaToDelete(FSInodeInfo inode,
      Block block, short replicationFactor,
      Collection<DatanodeDescriptor> first,
      Collection<DatanodeDescriptor> second) {

    DatanodeDescriptor chosenNode = null;
    try {
      String path = cachedFullPathNames.get(inode);
      FileInfo info = getFileInfo(path);
      if (info.type == FileType.NOT_RAID) {
        return super.chooseReplicaToDelete(
            inode, block, replicationFactor, first, second);
      }
      List<LocatedBlock> companionBlocks =
          getCompanionBlocks(path, info, block);
      if (companionBlocks == null || companionBlocks.size() == 0) {
        // Use the default method if it is not a valid raided or parity file
        return super.chooseReplicaToDelete(
            inode, block, replicationFactor, first, second);
      }
      // Delete from the first collection first
      // This ensures the number of unique rack of this block is not reduced
      Collection<DatanodeDescriptor> all = new HashSet<DatanodeDescriptor>();
      all.addAll(first);
      all.addAll(second);
      chosenNode = chooseReplicaToDelete(companionBlocks, all);
      if (chosenNode != null) {
        return chosenNode;
      }
      return super.chooseReplicaToDelete(
          inode, block, replicationFactor, first, second);
    } catch (Exception e) {
      LOG.debug("Failed to choose the correct replica to delete", e);
      return super.chooseReplicaToDelete(
          inode, block, replicationFactor, first, second);
    }
  }

  private DatanodeDescriptor chooseReplicaToDelete(
      Collection<LocatedBlock> companionBlocks,
      Collection<DatanodeDescriptor> dataNodes) throws IOException {

    if (dataNodes.isEmpty()) {
      return null;
    }
    // Count the number of replicas on each node and rack
    final Map<String, Integer> nodeCompanionBlockCount =
      countCompanionBlocks(companionBlocks, false);
    final Map<String, Integer> rackCompanionBlockCount =
      countCompanionBlocks(companionBlocks, true);

    NodeComparator comparator =
      new NodeComparator(nodeCompanionBlockCount, rackCompanionBlockCount);
    return Collections.max(dataNodes, comparator);
  }

  /**
   * Count how many companion blocks are on each datanode or the each rack
   * @param companionBlocks a collection of all the companion blocks
   * @param doRackCount count the companion blocks on the racks of datanodes
   * @param result the map from node name to the number of companion blocks
   */
  static Map<String, Integer> countCompanionBlocks(
      Collection<LocatedBlock> companionBlocks, boolean doRackCount) {
    Map<String, Integer> result = new HashMap<String, Integer>();
    for (LocatedBlock block : companionBlocks) {
      for (DatanodeInfo d : block.getLocations()) {
        String name = doRackCount ? d.getParent().getName() : d.getName();
        if (result.containsKey(name)) {
          int count = result.get(name) + 1;
          result.put(name, count);
        } else {
          result.put(name, 1);
        }
      }
    }
    return result;
  }

  /**
   * Compares the datanodes based on the number of companion blocks on the same
   * node and rack. If even, compare the remaining space on the datanodes.
   */
  class NodeComparator implements Comparator<DatanodeDescriptor> {
    private Map<String, Integer> nodeBlockCount;
    private Map<String, Integer> rackBlockCount;
    NodeComparator(Map<String, Integer> nodeBlockCount,
                           Map<String, Integer> rackBlockCount) {
      this.nodeBlockCount = nodeBlockCount;
      this.rackBlockCount = rackBlockCount;
    }
    @Override
    public int compare(DatanodeDescriptor d1, DatanodeDescriptor d2) {
      int res = compareBlockCount(d1, d2, nodeBlockCount);
      if (res != 0) {
        return res;
      }
      res = compareBlockCount(d1.getParent(), d2.getParent(), rackBlockCount);
      if (res != 0) {
        return res;
      }
      if (d1.getRemaining() > d2.getRemaining()) {
        return -1;
      }
      if (d1.getRemaining() < d2.getRemaining()) {
        return 1;
      }
      return 0;
    }
    private int compareBlockCount(Node node1, Node node2,
                                  Map<String, Integer> blockCount) {
      Integer count1 = blockCount.get(node1.getName());
      Integer count2 = blockCount.get(node2.getName());
      count1 = count1 == null ? 0 : count1;
      count2 = count2 == null ? 0 : count2;
      if (count1 > count2) {
        return 1;
      }
      if (count1 < count2) {
        return -1;
      }
      return 0;
    }
  }

  /**
   * Obtain the companion blocks of the give block
   * Companion blocks are defined as the blocks that can help recover each
   * others by using raid decoder.
   * @param path The path of the file contains the block
   * @param info The info of this file
   * @param block The given block
   *              null if it is the block which is currently being written to
   * @return the block locations of companion blocks
   */
  List<LocatedBlock> getCompanionBlocks(String path, FileInfo info, Block block)
      throws IOException {
    Codec codec = info.codec;
    switch (info.type) {
      case NOT_RAID:
        return Collections.emptyList();
      case HAR_TEMP_PARITY:
        return getCompanionBlocksForHarParityBlock(
            path, codec.parityLength, block);
      case TEMP_PARITY:
        return getCompanionBlocksForParityBlock(
            getSourceFile(path, codec.tmpParityDirectory),
            path, codec.parityLength, codec.stripeLength, block,
            codec.isDirRaid);
      case PARITY:
        return getCompanionBlocksForParityBlock(
            getSourceFile(path, codec.parityDirectory),
            path, codec.parityLength, codec.stripeLength, block, 
            codec.isDirRaid);
      case SOURCE:
        return getCompanionBlocksForSourceBlock(
            path,
            getParityFile(codec, path),
            codec.parityLength, codec.stripeLength, block,
            codec.isDirRaid);
    }
    return Collections.emptyList();
  }
  
	private String getSourceFile(String path, FileInfo info) {
		Codec codec = info.codec;
		try {
			switch (info.type) {
			case TEMP_PARITY:
				return getSourceFile(path, codec.tmpParityDirectory);
			case PARITY:
				return getSourceFile(path, codec.parityDirectory);
			}
		} catch (IOException e) {
			return null;
		}

		return null;
	}

  private List<LocatedBlock> getCompanionBlocksForHarParityBlock(
      String parity, int parityLength, Block block)
      throws IOException {
    int blockIndex = getBlockIndex(parity, block);
    // consider only parity file in this case because source file block
    // location is not easy to obtain
    List<LocatedBlock> parityBlocks = cachedLocatedBlocks.get(parity);
    List<LocatedBlock> result = new ArrayList<LocatedBlock>();
    int start = Math.max(0, blockIndex - parityLength + 1);
    int end = Math.min(parityBlocks.size(), blockIndex + parityLength);
    result.addAll(parityBlocks.subList(start, end));
    return result;
  }
  
  
  /**
   * Check if we will do the directory raid on the file.
   * 
   * @param fileBlocks, all the blocks of the file.
   * @return
   */
  private boolean canDoDirectoryRaid(List<LocatedBlock> fileBlocks) {
    long minFileSize = conf.getLong(RaidNode.MINIMUM_RAIDABLE_FILESIZE_KEY,
        RaidNode.MINIMUM_RAIDABLE_FILESIZE);
    long fileSize = 0;
    for (LocatedBlock block : fileBlocks) {
      fileSize += block.getBlock().getNumBytes();
      if (fileSize >= minFileSize) {
        return true;
      }
    }
    return false;
  }

  private List<LocatedBlock> getCompanionBlocksForParityBlock(
      String src, String parity, int parityLength, int stripeLength, 
      Block block, boolean isDirRaid)
      throws IOException {
    int blockIndex = getBlockIndex(parity, block);
    List<LocatedBlock> result = new ArrayList<LocatedBlock>();
    List<LocatedBlock> parityBlocks = cachedLocatedBlocks.get(parity);
    int stripeIndex = blockIndex / parityLength;
    int parityStart = stripeIndex * parityLength;
    int parityEnd = Math.min(parityStart + parityLength,
        parityBlocks.size());
    // for parity, always consider the neighbor blocks as companion blocks
    if (parityStart < parityBlocks.size()) {
      result.addAll(parityBlocks.subList(parityStart, parityEnd));
    }
    
    if (src == null) {
      return result;
    }
    
    // get the source blocks.
    List<LocatedBlock> sourceBlocks;
    int sourceStart = stripeIndex * stripeLength;
    int sourceEnd = sourceStart + stripeLength;
    
    if (!isDirRaid) { 
      sourceBlocks = cachedLocatedBlocks.get(src);
    } else {
      sourceBlocks = new ArrayList<LocatedBlock>();
      INode inode = namesystem.dir.getInode(src);
      INodeDirectory srcNode;
      if (inode.isDirectory()) {
        srcNode = (INodeDirectory) inode;
      } else {
        throw new IOException(
            "The source should be a directory in Dir-Raiding: " + src);
      }
      
      boolean found = false;
      // look for the stripe 
      for (INode child : srcNode.getChildren()) {
        if (child.isDirectory()) {
          throw new IOException("The source is not a leaf directory: " + src
              + ", contains a subdirectory: " + child.getLocalName());
        }
        
        List<LocatedBlock> childBlocks = 
            cachedLocatedBlocks.get(src + "/" + child.getLocalName());
        
        // check if we will do dir-raid on this file
        if (!canDoDirectoryRaid(childBlocks)) {
          continue;
        }
        
        if (childBlocks.size() < sourceStart && !found) {
          sourceStart -= childBlocks.size();
          sourceEnd -= childBlocks.size();
          continue;
        } else {
          found = true;
          sourceBlocks.addAll(childBlocks);
          if (sourceEnd <= sourceBlocks.size()) {
            break;
          }
        }
      }
    } 
    
    sourceEnd = Math.min(sourceEnd,
        sourceBlocks.size());
    if (sourceStart < sourceBlocks.size()) {
      result.addAll(sourceBlocks.subList(sourceStart, sourceEnd));
    }
      
    return result;
  }

  private List<LocatedBlock> getCompanionBlocksForSourceBlock(
      String src, String parity, int parityLength, int stripeLength, 
      Block block, boolean isDirRaid)
      throws IOException {
    int blockIndex = getBlockIndex(src, block);
    List<LocatedBlock> result = new ArrayList<LocatedBlock>();
    List<LocatedBlock> sourceBlocks; 
    
    int stripeIndex = 0;
    int sourceStart = 0;
    int sourceEnd = 0;
    
    if (!isDirRaid) {
      sourceBlocks = cachedLocatedBlocks.get(src);
      stripeIndex = blockIndex / stripeLength;
      sourceStart = stripeIndex * stripeLength;
      sourceEnd = Math.min(sourceStart + stripeLength, sourceBlocks.size());
    } else {
      // cache the candidate blocks.
      LocatedBlock[] tmpStripe = new LocatedBlock[stripeLength];
      int curIdx = 0;
      boolean found = false;
      
      sourceBlocks = new ArrayList<LocatedBlock>();
      INodeDirectory srcNode = namesystem.dir.getInode(src).getParent();
      String srcPath = new Path(src).getParent().toString();
      
      // look for the stripe
      for (INode child : srcNode.getChildren()) {
        if (child.isDirectory()) {
          throw new IOException("The raided-directory is not a leaf directory: "
              + srcPath + 
              ", contains a subdirectory: " + child.getLocalName());
        }
        
        String childName = srcPath + "/" + child.getLocalName();
        List<LocatedBlock> childBlocks = cachedLocatedBlocks.get(childName);
        
        // check if we will do dir-raid on this file
        if (!canDoDirectoryRaid(childBlocks)) {
          continue;
        }
        
        if (found) {
          if (sourceEnd <= sourceBlocks.size()) {
            break;
          }
          sourceBlocks.addAll(childBlocks);
        } else {
          int childBlockSize = childBlocks.size();
          
          /** 
           * If we find the target file, we will addAll the
           * cached blocks and the child blocks. 
           * And update the metrics like stripeIndex, sourceStart and sourceEnd.
           * 
           */
          if (childName.equals(src)) {
            found = true;
            for (int i=0; i<curIdx; i++) {
              sourceBlocks.add(tmpStripe[i]);
            }
            sourceBlocks.addAll(childBlocks);
            blockIndex += curIdx;
            
            stripeIndex += blockIndex / stripeLength;
            sourceStart = (blockIndex / stripeLength) * stripeLength;
            sourceEnd = sourceStart + stripeLength;
          } else {
            /**
             * If not find the target file, we will keep the current stripe
             * in the temp stripe cache.
             */
            /**
             * the childBlockSize is small, and we can fill them into 
             * current temp stripe cache.
             */
            if (curIdx + childBlockSize < stripeLength) {
              for (int i=0; i<childBlockSize; i++) {
                tmpStripe[curIdx++] = childBlocks.get(i);
              }
            } else {
              /**
               * The childBlockSize is not small, We need to calculate 
               * the place in the stripe cache, and copy the current stripe 
               * into the temp stripe cache.
               */
              stripeIndex += (curIdx + childBlockSize) / stripeLength;
              int childStart = ((curIdx + childBlockSize) / stripeLength) 
                  * stripeLength - curIdx;
              curIdx = 0;
              for (; childStart<childBlockSize;) {
                tmpStripe[curIdx++] = childBlocks.get(childStart++);
              }
              curIdx %= stripeLength;
            }
          }
        }
      }
      sourceEnd = Math.min(sourceEnd, sourceBlocks.size());
    } 
    
    if (sourceStart < sourceBlocks.size()) {
      result.addAll(sourceBlocks.subList(sourceStart, sourceEnd));
    }
      if (parity == null) {
      return result;
    }
    
    // get the parity blocks.
    List<LocatedBlock> parityBlocks = cachedLocatedBlocks.get(parity);
    int parityStart = stripeIndex * parityLength;
    int parityEnd = Math.min(parityStart + parityLength,
        parityBlocks.size());
    if (parityStart < parityBlocks.size()) {
      result.addAll(parityBlocks.subList(parityStart, parityEnd));
    }
    return result;
  }

  private int getBlockIndex(String file, Block block) throws IOException {
    List<LocatedBlock> blocks = cachedLocatedBlocks.get(file);
    // null indicates that this block is currently added. Return size()
    // as the index in this case
    if (block == null) {
      return blocks.size();
    }
    for (int i = 0; i < blocks.size(); i++) {
      if (blocks.get(i).getBlock().equals(block)) {
        return i;
      }
    }
    throw new IOException("Cannot locate " + block + " in file " + file);
  }

  /**
   * Cache results for FSInodeInfo.getFullPathName()
   */
  static class CachedFullPathNames {
    private Cache<INodeWithHashCode, String> cacheInternal;
    private FSNamesystem namesystem;

    CachedFullPathNames(final Configuration conf, final FSNamesystem namesystem) {
      this.namesystem = namesystem;
      this.cacheInternal = new Cache<INodeWithHashCode, String>(conf) {  
        @Override
        public String getDirectly(INodeWithHashCode inode) throws IOException {
          namesystem.readLock();
          try {
            return inode.getFullPathName();
          } finally {
            namesystem.readUnlock();
          }
        }
      };
    }
      
    private static class INodeWithHashCode {
      FSInodeInfo inode;
      INodeWithHashCode(FSInodeInfo inode) {
        this.inode = inode;
      }
      @Override
      public boolean equals(Object obj) {
        return inode == obj;
      }
      @Override
      public int hashCode() {
        return System.identityHashCode(inode);
      }
      String getFullPathName() {
        return inode.getFullPathName();
      }
    }

    public String get(FSInodeInfo inode) throws IOException {
      return cacheInternal.get(new INodeWithHashCode(inode));
    }
  }

  /**
   * Cache results for FSNamesystem.getBlockLocations()
   */
  static class CachedLocatedBlocks extends Cache<String, List<LocatedBlock>> {
    private FSNamesystem namesystem;

    CachedLocatedBlocks(Configuration conf, FSNamesystem namesystem) {
      super(conf);
      this.namesystem = namesystem;
    }

    @Override
    public List<LocatedBlock> getDirectly(String file) throws IOException {
      INodeFile inode = namesystem.dir.getFileINode(file);
      // Note that the list is generated. It is not the internal data of inode.
      List<LocatedBlock> result = inode == null ?
          new ArrayList<LocatedBlock>() :
          namesystem.getBlockLocationsInternal(inode, 0, Long.MAX_VALUE,
                                       Integer.MAX_VALUE).getLocatedBlocks();
      if (result == null) {
        return Collections.emptyList(); 
      }
      return result;
    }
   
    public void addBlock(String file, LocatedBlock lb) {
    	try {
			List<LocatedBlock> list = this.get(file);
			list.add(lb);
			this.update(file, list);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    }
  }

  private static abstract class Cache<K, V> {
    private Map<K, ValueWithTime> cache;
    final private long cacheTimeout;
    final private int maxEntries;
    // The timeout is long but the consequence of stale value is not serious
    Cache(Configuration conf) {
      this.cacheTimeout = 
        conf.getLong("raid.blockplacement.cache.timeout", 5000L); // 5 seconds
      this.maxEntries =
        conf.getInt("raid.blockplacement.cache.size", 1000);  // 1000 entries
      Map<K, ValueWithTime> map = new LinkedHashMap<K, ValueWithTime>(
          maxEntries, 0.75f, true) {
        private static final long serialVersionUID = 1L;
          @Override
          protected boolean removeEldestEntry(
            Map.Entry<K, ValueWithTime> eldest) {
            return size() > maxEntries;
          }
        };
      this.cache = Collections.synchronizedMap(map);
    }

    // Note that this method may hold FSNamesystem.readLock() and it may
    // be called inside FSNamesystem.writeLock(). If we make this method
    // synchronized, it will deadlock.
    abstract protected V getDirectly(K key) throws IOException;

    void update(K key, V value) {
    	ValueWithTime vt = cache.get(key);
    	vt.value = value;
    	cache.put(key, vt);
    }
    
    public V get(K key) throws IOException {
      // The method is not synchronized so we may get some stale value here but
      // it's OK.
      ValueWithTime result = cache.get(key);
      long now = System.currentTimeMillis();
      if (result != null &&
          now - result.cachedTime < cacheTimeout) {
        return result.value;
      }
      result = new ValueWithTime();
      result.value = getDirectly(key);
      result.cachedTime = now;
      cache.put(key, result);
      return result.value;
    }
    private class ValueWithTime {
      V value = null;
      long cachedTime = 0L;
    }
  }

  /**
   * Get path for the corresponding source file for a valid parity
   * file. Returns null if it does not exists
   * @param parity the toUri path of the parity file
   * @return the toUri path of the source file
   */
  String getSourceFile(String parity, String prefix) throws IOException {
    if (isHarFile(parity)) {
      return null;
    }
    // remove the prefix
    String src = parity.substring(prefix.length());
    if (namesystem.dir.getInode(src) == null) {
      return null;
    }
    return src;
  }

  /**
   * Get path for the parity file. Returns null if it does not exists
   * @param codec the codec of the parity file.
   * @return the toUri path of the parity file
   */
  private String getParityFile(Codec codec, String src)
      throws IOException {
    String parity;
    if (codec.isDirRaid) {
      String parent = new Path(src).getParent().toString();
      parity = codec.parityDirectory + parent;
    } else {
      parity = codec.parityDirectory + src;
    }
    
    if (namesystem.dir.getInode(parity) == null) {
      return null;
    }
    return parity;
  }

  private boolean isHarFile(String path) {
    return path.lastIndexOf(RaidNode.HAR_SUFFIX) != -1;
  }

  class FileInfo {
    FileInfo(FileType type, Codec codec) {
      this.type = type;
      this.codec = codec;
    }
    final FileType type;
    final Codec codec;
  }

  enum FileType {
	TO_RAID,
    NOT_RAID,
    HAR_TEMP_PARITY,
    TEMP_PARITY,
    PARITY,
    SOURCE,
  }

	/**
	 * Return raid information about a file, for example if this file is the
	 * source file, parity file, or not raid
	 * 
	 * @param path
	 *            file name
	 * @return raid information
	 * @throws IOException
	 */
	protected FileInfo getFileInfo(String path) throws IOException {
		if (this.policyConfig == null) {
			initPolicyConfig();
		}
		for (Codec c : Codec.getCodecs()) {
			if (path.startsWith(c.tmpHarDirectory + Path.SEPARATOR)) {
				return new FileInfo(FileType.HAR_TEMP_PARITY, c);
			}
			if (path.startsWith(c.tmpParityDirectory + Path.SEPARATOR)) {
				String tmpStr = path.substring(c.tmpParityDirectory.length() + 1);
				String str = tmpStr.substring(tmpStr.indexOf("/"));
				if(getSourceFile(str, c.parityDirectory) != null)
					return new FileInfo(FileType.TEMP_PARITY, c);
				else 
					return new FileInfo(FileType.NOT_RAID, null);
			}
			if (path.startsWith(c.parityDirectory + Path.SEPARATOR)) {
				if(getSourceFile(path, c.parityDirectory) != null)
					return new FileInfo(FileType.PARITY, c);
				else 
					return new FileInfo(FileType.NOT_RAID, null);
			}
			
			String parity = getParityFile(c, path);
			if (parity != null) {
				return new FileInfo(FileType.SOURCE, c);
			}
		}

		for (org.apache.hadoop.raid.protocol.PolicyInfo policy : policyConfig
				.getAllPolicies()) {
			String srcPath = policy.getSrcPath().toString();
			int index = srcPath.lastIndexOf(':');
			String src = srcPath.substring(index + 1);
			index = src.indexOf('/');
			src = src.substring(index);
			if (path.startsWith(src))
				return new FileInfo(FileType.TO_RAID, Codec.getCodec(policy
						.getCodecId()));
		}

		return new FileInfo(FileType.NOT_RAID, null);
	}
}