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

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.SocketChannel;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import org.apache.hadoop.util.DataChecksum;
import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.apache.hadoop.hdfs.protocol.datatransfer.*;
import org.apache.hadoop.hdfs.protocol.DatanodeInfo;
import org.apache.hadoop.hdfs.protocol.ExtendedBlock;
import org.apache.hadoop.hdfs.protocol.HdfsConstants;
import org.apache.hadoop.hdfs.protocol.HdfsConstants.DatanodeReportType;
import org.apache.hadoop.hdfs.protocol.LocatedBlock;
import org.apache.hadoop.hdfs.protocol.LocatedBlocks;
import org.apache.hadoop.hdfs.server.common.HdfsServerConstants;
import org.apache.hadoop.hdfs.server.datanode.BlockMetadataHeader;
import org.apache.hadoop.hdfs.server.datanode.RaidBlockSender;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.hdfs.RaidDFSUtil;
import org.apache.hadoop.util.Progressable;
import org.apache.hadoop.net.NetUtils;

import org.apache.hadoop.raid.RaidNode;
import org.apache.hadoop.raid.RaidUtils;


/**
 * contains the core functionality of the block fixer
 *
 * configuration options:
 * raid.blockfix.classname         - the class name of the block fixer 
 *                                   implementation to use
 *
 * raid.blockfix.interval          - interval between checks for corrupt files
 *
 * raid.blockfix.history.interval  - interval before fixing same file again
 *
 * raid.blockfix.read.timeout      - read time out
 *
 * raid.blockfix.write.timeout     - write time out
 */
public abstract class BlockFixer extends Configured implements Runnable {

  public static final String BLOCKFIX_CLASSNAME = "raid.blockfix.classname";
  public static final String BLOCKFIX_INTERVAL = "raid.blockfix.interval";
  public static final String BLOCKFIX_HISTORY_INTERVAL =
    "raid.blockfix.history.interval";
  public static final String BLOCKFIX_READ_TIMEOUT =
    "raid.blockfix.read.timeout";
  public static final String BLOCKFIX_WRITE_TIMEOUT =
    "raid.blockfix.write.timeout";

  public static final long DEFAULT_BLOCKFIX_INTERVAL = 60 * 1000; // 1 min
  public static final long DEFAULT_BLOCKFIX_HISTORY_INTERVAL =
    60 * 60 * 1000; // 60 mins

  public static BlockFixer createBlockFixer(Configuration conf)
    throws ClassNotFoundException {
    try {
      // default to distributed block fixer
      Class<?> blockFixerClass =
        conf.getClass(BLOCKFIX_CLASSNAME, DistBlockFixer.class);
      if (!BlockFixer.class.isAssignableFrom(blockFixerClass)) {
        throw new ClassNotFoundException("not an implementation of blockfixer");
      }
      Constructor<?> constructor =
        blockFixerClass.getConstructor(new Class[] {Configuration.class} );
      return (BlockFixer) constructor.newInstance(conf);
    } catch (NoSuchMethodException e) {
      throw new ClassNotFoundException("cannot construct blockfixer", e);
    } catch (InstantiationException e) {
      throw new ClassNotFoundException("cannot construct blockfixer", e);
    } catch (IllegalAccessException e) {
      throw new ClassNotFoundException("cannot construct blockfixer", e);
    } catch (InvocationTargetException e) {
      throw new ClassNotFoundException("cannot construct blockfixer", e);
    }
  }

  private long numFilesFixed = 0;

  public volatile boolean running = true;

  // interval between checks for corrupt files
  protected long blockFixInterval;

  // interval before fixing same file again
  protected long historyInterval;

  public BlockFixer(Configuration conf) {
    super(conf);
    blockFixInterval =
      getConf().getLong(BLOCKFIX_INTERVAL, DEFAULT_BLOCKFIX_INTERVAL);
    historyInterval =
      getConf().getLong(BLOCKFIX_HISTORY_INTERVAL,
                        DEFAULT_BLOCKFIX_HISTORY_INTERVAL);

  }

  @Override
  public abstract void run();

  /**
   * returns the number of files that have been fixed by this block fixer
   */
  public synchronized long filesFixed() {
    return numFilesFixed;
  }

  /**
   * increments the number of files that have been fixed by this block fixer
   */
  protected synchronized void incrFilesFixed() {
    numFilesFixed++;
  }

  /**
   * increments the number of files that have been fixed by this block fixer
   */
  protected synchronized void incrFilesFixed(long incr) {
    if (incr < 0) {
      throw new IllegalArgumentException("cannot increment by negative value " +
                                         incr);
    }
    
    numFilesFixed += incr;
  }

  static boolean isSourceFile(Path p, String[] destPrefixes) {
    String pathStr = p.toUri().getPath();
    for (String destPrefix: destPrefixes) {
      if (pathStr.startsWith(destPrefix)) {
        return false;
      }
    }
    return true;
  }

  void filterUnfixableSourceFiles(Iterator<Path> it) throws IOException {
    String xorPrefix = RaidNode.xorDestinationPath(getConf()).toUri().getPath();
    if (!xorPrefix.endsWith(Path.SEPARATOR)) {
      xorPrefix += Path.SEPARATOR;
    }
    String rsPrefix = RaidNode.rsDestinationPath(getConf()).toUri().getPath();
    if (!rsPrefix.endsWith(Path.SEPARATOR)) {
      rsPrefix += Path.SEPARATOR;
    }
    String[] destPrefixes = new String[]{xorPrefix, rsPrefix};
    while (it.hasNext()) {
      Path p = it.next();
      if (isSourceFile(p, destPrefixes) &&
          RaidNode.xorParityForSource(p, getConf()) == null &&
          RaidNode.rsParityForSource(p, getConf()) == null) {
        it.remove();
      }
    }
  }

  /**
   * this class implements the actual fixing functionality
   * we keep this in a separate class so that 
   * the distributed block fixer can use it
   */ 
  static class BlockFixerHelper extends Configured {

    public static final Log LOG = LogFactory.getLog(BlockFixer.
                                                    BlockFixerHelper.class);

    private String xorPrefix;
    private String rsPrefix;
    private XOREncoder xorEncoder;
    private XORDecoder xorDecoder;
    private ReedSolomonEncoder rsEncoder;
    private ReedSolomonDecoder rsDecoder;

    public BlockFixerHelper(Configuration conf) throws IOException {
      super(conf);

      xorPrefix = RaidNode.xorDestinationPath(getConf()).toUri().getPath();
      if (!xorPrefix.endsWith(Path.SEPARATOR)) {
        xorPrefix += Path.SEPARATOR;
      }
      rsPrefix = RaidNode.rsDestinationPath(getConf()).toUri().getPath();
      if (!rsPrefix.endsWith(Path.SEPARATOR)) {
        rsPrefix += Path.SEPARATOR;
      }
      int stripeLength = RaidNode.getStripeLength(getConf());
      xorEncoder = new XOREncoder(getConf(), stripeLength);
      xorDecoder = new XORDecoder(getConf(), stripeLength);
      int parityLength = RaidNode.rsParityLength(getConf());
      rsEncoder = new ReedSolomonEncoder(getConf(), stripeLength, parityLength);
      rsDecoder = new ReedSolomonDecoder(getConf(), stripeLength, parityLength);

    }

    /**
     * checks whether file is xor parity file
     */
    boolean isXorParityFile(Path p) {
      String pathStr = p.toUri().getPath();
      if (pathStr.contains(RaidNode.HAR_SUFFIX)) {
        return false;
      }
      return pathStr.startsWith(xorPrefix);
    }

    /**
     * checks whether file is rs parity file
     */
    boolean isRsParityFile(Path p) {
      String pathStr = p.toUri().getPath();
      if (pathStr.contains(RaidNode.HAR_SUFFIX)) {
        return false;
      }
      return pathStr.startsWith(rsPrefix);
    }

    /**
     * Fix a file, do not report progess.
     *
     * @return true if file has been fixed, false if no fixing 
     * was necessary or possible.
     */
    boolean fixFile(Path srcPath) throws IOException {
      return fixFile(srcPath, new RaidUtils.DummyProgressable());
    }

    /**
     * Fix a file, report progess.
     *
     * @return true if file has been fixed, false if no fixing 
     * was necessary or possible.
     */
    boolean fixFile(Path srcPath, Progressable progress) throws IOException {

      if (RaidNode.isParityHarPartFile(srcPath)) {
        return processCorruptParityHarPartFile(srcPath, progress);
      }

      // The corrupted file is a XOR parity file
      if (isXorParityFile(srcPath)) {
        return processCorruptParityFile(srcPath, xorEncoder, progress);
      }

      // The corrupted file is a ReedSolomon parity file
      if (isRsParityFile(srcPath)) {
        return processCorruptParityFile(srcPath, rsEncoder, progress);
      }

      // The corrupted file is a source file
      RaidNode.ParityFilePair ppair =
        RaidNode.xorParityForSource(srcPath, getConf());
      Decoder decoder = null;
      if (ppair != null) {
        decoder = xorDecoder;
      } else  {
        ppair = RaidNode.rsParityForSource(srcPath, getConf());
        if (ppair != null) {
          decoder = rsDecoder;
        }
      }

      // If we have a parity file, process the file and fix it.
      if (ppair != null) {
        return processCorruptFile(srcPath, ppair, decoder, progress);
      }
      
      // there was nothing to do
      return false;
    }

    /**
     * Sorts source files ahead of parity files.
     */
    void sortCorruptFiles(List<Path> files) {
      // TODO: We should first fix the files that lose more blocks
      Comparator<Path> comp = new Comparator<Path>() {
        public int compare(Path p1, Path p2) {
          if (isXorParityFile(p2) || isRsParityFile(p2)) {
            // If p2 is a parity file, p1 is smaller.
            return -1;
          }
          if (isXorParityFile(p1) || isRsParityFile(p1)) {
            // If p1 is a parity file, p2 is smaller.
            return 1;
          }
          // If both are source files, they are equal.
          return 0;
        }
      };
      Collections.sort(files, comp);
    }

    /**
     * Returns a DistributedFileSystem hosting the path supplied.
     */
    protected DistributedFileSystem getDFS(Path p) throws IOException {
      return (DistributedFileSystem) p.getFileSystem(getConf());
    }

    /**
     * Reads through a corrupt source file fixing corrupt blocks on the way.
     * @param srcPath Path identifying the corrupt file.
     * @throws IOException
     * @return true if file has been fixed, false if no fixing 
     * was necessary or possible.
     */
    boolean processCorruptFile(Path srcPath, RaidNode.ParityFilePair parityPair,
                               Decoder decoder, Progressable progress)
      throws IOException {
      LOG.info("Processing corrupt file " + srcPath);
      
      DistributedFileSystem srcFs = getDFS(srcPath);
      FileStatus srcStat = srcFs.getFileStatus(srcPath);
      long blockSize = srcStat.getBlockSize();
      long srcFileSize = srcStat.getLen();
      String uriPath = srcPath.toUri().getPath();
      
      int numBlocksFixed = 0;
      List<LocatedBlock> corrupt =
        RaidDFSUtil.corruptBlocksInFile(srcFs, uriPath, 0, srcFileSize);
      if (corrupt.size() == 0) {
        return false;
      }
      for (LocatedBlock lb: corrupt) {
        ExtendedBlock corruptBlock = lb.getBlock();
        long corruptOffset = lb.getStartOffset();
        
        LOG.info("Found corrupt block " + corruptBlock +
                 ", offset " + corruptOffset);
        
        final long blockContentsSize =
          Math.min(blockSize, srcFileSize - corruptOffset);
        File localBlockFile =
          File.createTempFile(corruptBlock.getBlockName(), ".tmp");
        localBlockFile.deleteOnExit();
        
        try {
          decoder.recoverBlockToFile(srcFs, srcPath, parityPair.getFileSystem(),
                                     parityPair.getPath(), blockSize,
                                     corruptOffset, localBlockFile,
                                     blockContentsSize);
          
          // We have a the contents of the block, send them.
          DatanodeInfo datanode = chooseDatanode(lb.getLocations());
          computeMetadataAndSendFixedBlock(datanode, localBlockFile,
                                          lb, blockContentsSize);
          numBlocksFixed++;
        } finally {
          localBlockFile.delete();
        }
        progress.progress();
      }
      LOG.info("Fixed " + numBlocksFixed + " blocks in " + srcPath);
      return true;
    }

    /**
     * Fixes corrupt blocks in a parity file.
     * This function uses the corresponding source file to regenerate parity
     * file blocks.
     * @return true if file has been fixed, false if no fixing 
     * was necessary or possible.
     */
    boolean processCorruptParityFile(Path parityPath, Encoder encoder,
                                     Progressable progress)
      throws IOException {
      LOG.info("Processing corrupt file " + parityPath);
      Path srcPath = sourcePathFromParityPath(parityPath);
      if (srcPath == null) {
        LOG.warn("Unusable parity file " + parityPath);
        return false;
      }

      DistributedFileSystem parityFs = getDFS(parityPath);
      FileStatus parityStat = parityFs.getFileStatus(parityPath);
      long blockSize = parityStat.getBlockSize();
      long parityFileSize = parityStat.getLen();
      FileStatus srcStat = getDFS(srcPath).getFileStatus(srcPath);
      long srcFileSize = srcStat.getLen();

      // Check timestamp.
      if (srcStat.getModificationTime() != parityStat.getModificationTime()) {
        LOG.info("Mismatching timestamp for " + srcPath + " and " + parityPath +
                 ", moving on...");
        return false;
      }

      String uriPath = parityPath.toUri().getPath();
      int numBlocksFixed = 0;
      List<LocatedBlock> corrupt =
        RaidDFSUtil.corruptBlocksInFile(parityFs, uriPath, 0, parityFileSize);
      if (corrupt.size() == 0) {
        return false;
      }
      for (LocatedBlock lb: corrupt) {
        ExtendedBlock corruptBlock = lb.getBlock();
        long corruptOffset = lb.getStartOffset();
        
        LOG.info("Found corrupt block " + corruptBlock +
                 ", offset " + corruptOffset);
        
        File localBlockFile =
          File.createTempFile(corruptBlock.getBlockName(), ".tmp");
        localBlockFile.deleteOnExit();
        
        try {
          encoder.recoverParityBlockToFile(parityFs, srcPath, srcFileSize,
                                           blockSize, parityPath,
                                           corruptOffset, localBlockFile);
          // We have a the contents of the block, send them.
          DatanodeInfo datanode = chooseDatanode(lb.getLocations());
          computeMetadataAndSendFixedBlock(datanode, localBlockFile, lb,
                                          blockSize);
          
          numBlocksFixed++;
        } finally {
          localBlockFile.delete();
        }
        progress.progress();
      }
      LOG.info("Fixed " + numBlocksFixed + " blocks in " + parityPath);
      return true;
    }

    /**
     * Reads through a parity HAR part file, fixing corrupt blocks on the way.
     * A HAR block can contain many file blocks, as long as the HAR part file
     * block size is a multiple of the file block size.
     * @return true if file has been fixed, false if no fixing 
     * was necessary or possible.
     */
    boolean processCorruptParityHarPartFile(Path partFile,
                                            Progressable progress)
      throws IOException {
      LOG.info("Processing corrupt file " + partFile);
      // Get some basic information.
      DistributedFileSystem dfs = getDFS(partFile);
      FileStatus partFileStat = dfs.getFileStatus(partFile);
      long partFileSize = partFileStat.getLen();
      long partFileBlockSize = partFileStat.getBlockSize();
      LOG.info(partFile + " has block size " + partFileBlockSize);

      // Find the path to the index file.
      // Parity file HARs are only one level deep, so the index files is at the
      // same level as the part file.
      String harDirectory = partFile.toUri().getPath(); // Temporarily.
      harDirectory =
        harDirectory.substring(0, harDirectory.lastIndexOf(Path.SEPARATOR));
      Path indexFile = new Path(harDirectory + "/" + HarIndex.indexFileName);
      FileStatus indexStat = dfs.getFileStatus(indexFile);
      // Parses through the HAR index file.
      HarIndex harIndex = new HarIndex(dfs.open(indexFile), indexStat.getLen());

      String uriPath = partFile.toUri().getPath();
      int numBlocksFixed = 0;
      List<LocatedBlock> corrupt =
        RaidDFSUtil.corruptBlocksInFile(dfs, uriPath, 0, partFileSize);
      if (corrupt.size() == 0) {
        return false;
      }
      for (LocatedBlock lb: corrupt) {
        ExtendedBlock corruptBlock = lb.getBlock();
        long corruptOffset = lb.getStartOffset();

        File localBlockFile =
          File.createTempFile(corruptBlock.getBlockName(), ".tmp");
        localBlockFile.deleteOnExit();
        processCorruptParityHarPartBlock(dfs, partFile, corruptBlock,
                                         corruptOffset, partFileStat, harIndex,
                                         localBlockFile, progress);
        // Now we have recovered the part file block locally, send it.
        try {
          DatanodeInfo datanode = chooseDatanode(lb.getLocations());
          computeMetadataAndSendFixedBlock(datanode, localBlockFile,
                                          lb, localBlockFile.length());
          numBlocksFixed++;
        } finally {
          localBlockFile.delete();
        }
        progress.progress();
      }
      LOG.info("Fixed " + numBlocksFixed + " blocks in " + partFile);
      return true;
    }

    /**
     * This fixes a single part file block by recovering in sequence each
     * parity block in the part file block.
     */
    private void processCorruptParityHarPartBlock(FileSystem dfs, Path partFile,
                                                  ExtendedBlock corruptBlock,
                                                  long corruptOffset,
                                                  FileStatus partFileStat,
                                                  HarIndex harIndex,
                                                  File localBlockFile,
                                                  Progressable progress)
      throws IOException {
      String partName = partFile.toUri().getPath(); // Temporarily.
      partName = partName.substring(1 + partName.lastIndexOf(Path.SEPARATOR));

      OutputStream out = new FileOutputStream(localBlockFile);

      try {
        // A HAR part file block could map to several parity files. We need to
        // use all of them to recover this block.
        final long corruptEnd = Math.min(corruptOffset +
                                         partFileStat.getBlockSize(),
                                         partFileStat.getLen());
        for (long offset = corruptOffset; offset < corruptEnd; ) {
          HarIndex.IndexEntry entry = harIndex.findEntry(partName, offset);
          if (entry == null) {
            String msg = "Corrupt index file has no matching index entry for " +
              partName + ":" + offset;
            LOG.warn(msg);
            throw new IOException(msg);
          }
          Path parityFile = new Path(entry.fileName);
          Encoder encoder;
          if (isXorParityFile(parityFile)) {
            encoder = xorEncoder;
          } else if (isRsParityFile(parityFile)) {
            encoder = rsEncoder;
          } else {
            String msg = "Could not figure out parity file correctly";
            LOG.warn(msg);
            throw new IOException(msg);
          }
          Path srcFile = sourcePathFromParityPath(parityFile);
          FileStatus srcStat = dfs.getFileStatus(srcFile);
          if (srcStat.getModificationTime() != entry.mtime) {
            String msg = "Modification times of " + parityFile + " and " +
              srcFile + " do not match.";
            LOG.warn(msg);
            throw new IOException(msg);
          }
          long corruptOffsetInParity = offset - entry.startOffset;
          LOG.info(partFile + ":" + offset + " maps to " +
                   parityFile + ":" + corruptOffsetInParity +
                   " and will be recovered from " + srcFile);
          encoder.recoverParityBlockToStream(dfs, srcFile, srcStat.getLen(),
                                             srcStat.getBlockSize(), parityFile,
                                             corruptOffsetInParity, out);
          // Finished recovery of one parity block. Since a parity block has the
          // same size as a source block, we can move offset by source block size.
          offset += srcStat.getBlockSize();
          LOG.info("Recovered " + srcStat.getBlockSize() + " part file bytes ");
          if (offset > corruptEnd) {
            String msg =
              "Recovered block spills across part file blocks. Cannot continue.";
            throw new IOException(msg);
          }
          progress.progress();
        }
      } finally {
        out.close();
      }
    }

    /**
     * Choose a datanode (hostname:portnumber). The datanode is chosen at
     * random from the live datanodes.
     * @param locationsToAvoid locations to avoid.
     * @return A datanode
     * @throws IOException
     */
    private DatanodeInfo chooseDatanode(DatanodeInfo[] locationsToAvoid)
      throws IOException {
      DistributedFileSystem dfs = getDFS(new Path("/"));
      DatanodeInfo[] live =
        dfs.getClient().datanodeReport(DatanodeReportType.LIVE);
      LOG.info("Choosing a datanode from " + live.length +
               " live nodes while avoiding " + locationsToAvoid.length);
      Random rand = new Random();
      DatanodeInfo chosen = null;
      int maxAttempts = 1000;
      for (int i = 0; i < maxAttempts && chosen == null; i++) {
        int idx = rand.nextInt(live.length);
        chosen = live[idx];
        for (DatanodeInfo avoid: locationsToAvoid) {
          if (chosen.name.equals(avoid.name)) {
            LOG.info("Avoiding " + avoid.name);
            chosen = null;
            break;
          }
        }
      }
      if (chosen == null) {
        throw new IOException("Could not choose datanode");
      }
      LOG.info("Choosing datanode " + chosen.name);
      return chosen;
    }

    /**
     * Reads data from the data stream provided and computes metadata.
     */
    static DataInputStream computeMetadata(Configuration conf,
                                           InputStream dataStream)
      throws IOException {
      ByteArrayOutputStream mdOutBase = new ByteArrayOutputStream(1024*1024);
      DataOutputStream mdOut = new DataOutputStream(mdOutBase);
      
      // First, write out the version.
      mdOut.writeShort(BlockMetadataHeader.VERSION);
      
      // Create a summer and write out its header.
      int bytesPerChecksum = conf.getInt("io.bytes.per.checksum", 512);
      DataChecksum sum =
        DataChecksum.newDataChecksum(DataChecksum.CHECKSUM_CRC32,
                                     bytesPerChecksum);
      sum.writeHeader(mdOut);
      
      // Buffer to read in a chunk of data.
      byte[] buf = new byte[bytesPerChecksum];
      // Buffer to store the checksum bytes.
      byte[] chk = new byte[sum.getChecksumSize()];
      
      // Read data till we reach the end of the input stream.
      int bytesSinceFlush = 0;
      while (true) {
        // Read some bytes.
        int bytesRead = dataStream.read(buf, bytesSinceFlush,
                                        bytesPerChecksum-bytesSinceFlush);
        if (bytesRead == -1) {
          if (bytesSinceFlush > 0) {
            boolean reset = true;
            sum.writeValue(chk, 0, reset); // This also resets the sum.
            // Write the checksum to the stream.
            mdOut.write(chk, 0, chk.length);
            bytesSinceFlush = 0;
          }
          break;
        }
        // Update the checksum.
        sum.update(buf, bytesSinceFlush, bytesRead);
        bytesSinceFlush += bytesRead;
        
        // Flush the checksum if necessary.
        if (bytesSinceFlush == bytesPerChecksum) {
          boolean reset = true;
          sum.writeValue(chk, 0, reset); // This also resets the sum.
          // Write the checksum to the stream.
          mdOut.write(chk, 0, chk.length);
          bytesSinceFlush = 0;
        }
      }
      
      byte[] mdBytes = mdOutBase.toByteArray();
      return new DataInputStream(new ByteArrayInputStream(mdBytes));
    }

    private void computeMetadataAndSendFixedBlock(DatanodeInfo datanode,
                                                  File localBlockFile,
                                                  LocatedBlock block,
                                                  long blockSize)
      throws IOException {

      LOG.info("Computing metdata");
      InputStream blockContents = null;
      DataInputStream blockMetadata = null;
      try {
        blockContents = new FileInputStream(localBlockFile);
        blockMetadata = computeMetadata(getConf(), blockContents);
        blockContents.close();
        // Reopen
        blockContents = new FileInputStream(localBlockFile);
        sendFixedBlock(datanode, blockContents, blockMetadata, block,
                       blockSize);
      } finally {
        if (blockContents != null) {
          blockContents.close();
          blockContents = null;
        }
        if (blockMetadata != null) {
          blockMetadata.close();
          blockMetadata = null;
        }
      }
    }

    /**
     * Send a generated block to a datanode.
     * @param datanode Chosen datanode name in host:port form.
     * @param blockContents Stream with the block contents.
     * @param corruptBlock Block identifying the block to be sent.
     * @param blockSize size of the block.
     * @throws IOException
     */
    private void sendFixedBlock(DatanodeInfo datanode,
                                final InputStream blockContents,
                                DataInputStream metadataIn,
                                LocatedBlock block, long blockSize)
      throws IOException {
      InetSocketAddress target = NetUtils.createSocketAddr(datanode.name);
      Socket sock = SocketChannel.open().socket();
      
      int readTimeout =
        getConf().getInt(BLOCKFIX_READ_TIMEOUT,
                         HdfsServerConstants.READ_TIMEOUT);
      NetUtils.connect(sock, target, readTimeout);
      sock.setSoTimeout(readTimeout);
      
      int writeTimeout = getConf().getInt(BLOCKFIX_WRITE_TIMEOUT,
                                          HdfsServerConstants.WRITE_TIMEOUT);
      
      OutputStream baseStream = NetUtils.getOutputStream(sock, writeTimeout);
      DataOutputStream out = 
        new DataOutputStream(new BufferedOutputStream(baseStream,
                                                      HdfsConstants.
                                                      SMALL_BUFFER_SIZE));
      
      boolean corruptChecksumOk = false;
      boolean chunkOffsetOK = false;
      boolean verifyChecksum = true;
      boolean transferToAllowed = false;
      
      try {
        LOG.info("Sending block " + block.getBlock() +
                 " from " + sock.getLocalSocketAddress().toString() +
                 " to " + sock.getRemoteSocketAddress().toString() +
                 " " + blockSize + " bytes");
        RaidBlockSender blockSender =
          new RaidBlockSender(block.getBlock(), blockSize, 0, blockSize,
                              corruptChecksumOk, chunkOffsetOK, verifyChecksum,
                              transferToAllowed, metadataIn,
                              new RaidBlockSender.InputStreamFactory() {
                                @Override
                                public InputStream
                                  createStream(long offset) throws IOException {
                                  // we are passing 0 as the offset above,
                                  // so we can safely ignore
                                  // the offset passed
                                  return blockContents;
                                }
                              });
        
        DatanodeInfo[] nodes = new DatanodeInfo[]{datanode};
        new Sender(out).writeBlock(block.getBlock(), block.getBlockToken(), "",
            nodes, null, BlockConstructionStage.PIPELINE_SETUP_CREATE,
            1, 0L, blockSize, 0L, DataChecksum.newDataChecksum(metadataIn));
        blockSender.sendBlock(out, baseStream);
        
        LOG.info("Sent block " + block.getBlock() + " to " + datanode.name);
      } finally {
        out.close();
      }
    }

    /**
     * returns the source file corresponding to a parity file
     */
    Path sourcePathFromParityPath(Path parityPath) {
      String parityPathStr = parityPath.toUri().getPath();
      if (parityPathStr.startsWith(xorPrefix)) {
        // Remove the prefix to get the source file.
        String src = parityPathStr.replaceFirst(xorPrefix, "/");
        return new Path(src);
      } else if (parityPathStr.startsWith(rsPrefix)) {
        // Remove the prefix to get the source file.
        String src = parityPathStr.replaceFirst(rsPrefix, "/");
        return new Path(src);
      }
      return null;
    }

    /**
     * Returns the corrupt blocks in a file.
     */
    List<LocatedBlock> corruptBlocksInFile(DistributedFileSystem fs,
                                           String uriPath, FileStatus stat)
      throws IOException {
      List<LocatedBlock> corrupt = new LinkedList<LocatedBlock>();
      LocatedBlocks locatedBlocks =
        RaidDFSUtil.getBlockLocations(fs, uriPath, 0, stat.getLen());
      for (LocatedBlock b: locatedBlocks.getLocatedBlocks()) {
        if (b.isCorrupt() ||
            (b.getLocations().length == 0 && b.getBlockSize() > 0)) {
          corrupt.add(b);
        }
      }
      return corrupt;
    }
  }

}

