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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.util.Random;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.ContentSummary;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.DFSTestUtil;
import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.hdfs.protocol.LocatedBlocks;
import org.apache.hadoop.raid.RaidCodec;
import org.apache.hadoop.raid.RaidCodecBuilder;
import org.apache.hadoop.security.UnixUserGroupInformation;
import org.apache.hadoop.hdfs.server.namenode.NameNode;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class TestMergeFile {
  public static final Log LOG = LogFactory.getLog(TestMergeFile.class);

  private static final short REPL_FACTOR = 2;
  
  private MiniDFSCluster cluster;
  private NameNode nn;
  private DistributedFileSystem dfs;
  private DistributedFileSystem userdfs;
  private static long blockSize = 512;
  private static int numDataBlocks = 6;
  private static int numRSParityBlocks = 3;
  private static Configuration conf;
  private static Random rand = new Random();
  private static UnixUserGroupInformation USER1;
  private static int id = 0;
  static {
    conf = new Configuration();
    conf.setLong("dfs.block.size", blockSize);
    conf.setBoolean("dfs.permissions", true);
  }
  
  @Before
  public void startUpCluster() throws IOException {
    RaidCodecBuilder.loadDefaultFullBlocksCodecs(conf, numRSParityBlocks,
        numDataBlocks);
    cluster = new MiniDFSCluster(conf, REPL_FACTOR, true, null);
    assertNotNull("Failed Cluster Creation", cluster);
    cluster.waitClusterUp();
    dfs = (DistributedFileSystem) cluster.getFileSystem();
    assertNotNull("Failed to get FileSystem", dfs);
    nn = cluster.getNameNode();
    assertNotNull("Failed to get NameNode", nn);
    Configuration newConf = new Configuration(conf);
    USER1 = new UnixUserGroupInformation("foo", new String[] {"bar" });
    UnixUserGroupInformation.saveToConf(newConf,
        UnixUserGroupInformation.UGI_PROPERTY_NAME, USER1);
    userdfs = (DistributedFileSystem)FileSystem.get(newConf); // login as ugi
  }

  @After
  public void shutDownCluster() throws IOException {
    if(dfs != null) {
      dfs.close();
    }
    if (userdfs != null) {
      userdfs.close();
    }
    if(cluster != null) {
      cluster.shutdownDataNodes();
      cluster.shutdown();
    }
  }
  
  public void mergeFile(Path parity, Path source, String codecId, int[] checksums,
      String exceptionMessage) throws Exception {
    mergeFile(dfs, parity, source, codecId, checksums, exceptionMessage);
  }
  
  public void mergeFile(DistributedFileSystem fs, Path parity, Path source, 
      String codecId, int[] checksums, String exceptionMessage) throws Exception {
    try {
      fs.merge(parity, source, codecId, checksums);
    } catch (Exception e) {
      if (exceptionMessage == null) {
        // This is not expected
        throw e;
      }
      assertTrue("Exception " + e.getMessage() + " doesn't match " + 
                 exceptionMessage, e.getMessage().contains(exceptionMessage));
    }
  }
  
  @Test
  public void testMergeXORFile() throws Exception {
    mergeFile(12, 2, (short)2, "xor");
    mergeFile(9, 2, (short)1, "xor");
    mergeFile(3, 1, (short)2, "xor");
  }
  
  @Test
  public void testMergeRSFile() throws Exception {
    mergeFile(12, 6, (short)1, "rs");
    mergeFile(9, 6, (short)1, "rs");
    mergeFile(3, 3, (short)2, "rs");
  }
  
  /**
   * @return current file status of file
   */
  public static FileStatus verifyMergeFiles(DistributedFileSystem fileSys, FileStatus statBefore, 
      LocatedBlocks lbsBefore, Path source, long fileLen, long crc) throws Exception {
    FileStatus statAfter  = fileSys.getFileStatus(source);
    LocatedBlocks lbsAfter = fileSys.getLocatedBlocks(source, 0, fileLen);
    // Verify file stat
    assertEquals(statBefore.getBlockSize(), statAfter.getBlockSize());
    assertEquals(statBefore.getLen(), statAfter.getLen());
    assertEquals(statBefore.getReplication(), statAfter.getReplication());
    
    // Verify getLocatedBlocks
    
    assertEquals(lbsBefore.getLocatedBlocks().size(), 
        lbsAfter.getLocatedBlocks().size());
    for (int i = 0; i < lbsBefore.getLocatedBlocks().size(); i++) {
      assertEquals(lbsBefore.get(i).getBlock(), lbsAfter.get(i).getBlock());
    }
    
    // Verify file content
    assertTrue("File content matches", DFSTestUtil.validateFile(fileSys, 
        statBefore.getPath(), statBefore.getLen(), crc));
    return statAfter;
  }
  
  public void mergeFile(int numBlocks, int parityBlocks, short sourceRepl,
      String codecId) throws Exception {
    LOG.info("RUNNING testMergeFile numBlocks=" + numBlocks + 
        " parityBlocks=" + parityBlocks + " sourceRepl=" + sourceRepl +
        " codecId=" + codecId);
    id++;
    long fileLen = blockSize * numBlocks;
    long parityLen = blockSize * parityBlocks;
    Path dir = new Path ("/user/facebook" + id);
    assertTrue(dfs.mkdirs(dir));
    Path source = new Path(dir, "1");
    Path dest = new Path(dir, "2");
    long crc = DFSTestUtil.createFile(dfs, source, fileLen, sourceRepl, 1);
    
    Path parityDir = new Path("/raid/user/facebook" + id);
    assertTrue(dfs.mkdirs(parityDir));
    RaidCodec codec = RaidCodec.getCodec(codecId);
    Path parity = new Path(parityDir, "1");
    DFSTestUtil.createFile(dfs, parity, parityLen,
        codec.parityReplication, 1);
    int[] checksums = new int[numBlocks];
    for (int i = 0; i < numBlocks; i++) {
      checksums[i] = rand.nextInt();
    }
    
    ContentSummary cBefore = dfs.getContentSummary(dir);
    ContentSummary cParityBefore = dfs.getContentSummary(parityDir);
    FileStatus statBefore = dfs.getFileStatus(source);
    LocatedBlocks lbsBefore = dfs.getLocatedBlocks(source, 0, fileLen);
    dfs.setTimes(parity, statBefore.getModificationTime(), 0);
    
    // now merge
    dfs.merge(parity, source, codecId, checksums);
    
    ContentSummary cAfter = dfs.getContentSummary(dir);
    ContentSummary cParityAfter = dfs.getContentSummary(parityDir);
    
    // verify directory stat
    assertEquals("File count doesn't change", cBefore.getFileCount(),
        cAfter.getFileCount());
    assertEquals("Space consumed is increased", 
        cBefore.getSpaceConsumed() + parityLen * codec.parityReplication,
        cAfter.getSpaceConsumed());
    assertEquals("Parity file is removed", cParityBefore.getFileCount() - 1,
        cParityAfter.getFileCount());
    assertEquals("Space consumed is 0", 0, cParityAfter.getSpaceConsumed());
   
    // Verify parity is removed
    assertTrue(!dfs.exists(parity));
    
    verifyMergeFiles(dfs, statBefore, lbsBefore, source, fileLen, crc);
   
    LocatedBlocks lbsAfter = dfs.getLocatedBlocks(source, blockSize, fileLen);
    assertEquals(numBlocks - 1, lbsAfter.getLocatedBlocks().size());
    for (int i = 0; i < numBlocks - 1; i++) {
      assertEquals(lbsBefore.get(i + 1).getBlock(), lbsAfter.get(i).getBlock());
    }
    assertTrue("Should not be able to hardlink a raided file", 
        !dfs.hardLink(source, dest));
  }
  
  @Test
  public void testMergeIllegalCases() throws Exception {
    LOG.info("Running testMergeIllegalCases");
    int numBlocks = 6;
    long fileLen = blockSize * numBlocks;
    
    Path dir = new Path ("/user/facebook");
    assertTrue(dfs.mkdirs(dir));
    Path source = new Path(dir, "1");
    Path dest = new Path(dir, "2");
    DFSTestUtil.createFile(dfs, source, fileLen, REPL_FACTOR, 1);
    FileStatus stat = dfs.getFileStatus(source);
    Path raidDir = new Path("/raid/user/facebook");
    assertTrue(dfs.mkdirs(raidDir));
    Path badParity = new Path(raidDir, "1");
    DFSTestUtil.createFile(dfs, badParity, blockSize * 2,
        (short)1, 1);
    int[] checksums = new int[numBlocks];
    for (int i = 0; i < numBlocks; i++) {
      checksums[i] = rand.nextInt();
    }
    Path emptyFile = new Path("/empty");
    DFSTestUtil.createFile(dfs, emptyFile, 0L, REPL_FACTOR, 1);
    
    mergeFile(badParity, source, "xor", null, 
        "merge: checksum array is empty or null");
    mergeFile(badParity, source, "nonexist", checksums,
        "merge: codec nonexist doesn't exist");
    
    dfs.setOwner(source, "foo", "bar");
    dfs.setOwner(badParity, "foo", "bar");
    dfs.setOwner(raidDir, "foo", "bar");
    
    LOG.info("Disallow write on " + source);
    dfs.setPermission(source, new FsPermission((short)0577));
    mergeFile(userdfs, badParity, source, "rs", checksums, "Permission denied");
    
    LOG.info("Enable write on " + source + " and disable read on " + badParity);
    dfs.setPermission(source, new FsPermission((short)0777));
    dfs.setPermission(badParity, new FsPermission((short)0377));
    mergeFile(userdfs, badParity, source, "rs", checksums, "Permission denied");
    
    LOG.info("Enable read on " + badParity + " and disable write on " + raidDir);
    dfs.setPermission(badParity, new FsPermission((short)0777));
    dfs.setPermission(raidDir, new FsPermission((short)0577));
    mergeFile(userdfs, badParity, source, "rs", checksums, "Permission denied");
    dfs.setPermission(raidDir, new FsPermission((short)0777));
    
    LOG.info("Test different types of files");
    mergeFile(new Path("/nonexist"), source, "rs", checksums,
        "merge: source file or parity file doesn't exist");
    mergeFile(badParity, new Path("/nonexist"), "rs", checksums,
        "merge: source file or parity file doesn't exist");
    
    mergeFile(raidDir, source, "rs", checksums,
        "merge: source file or parity file is a directory");
    mergeFile(badParity, dir, "rs", checksums,
        "merge: source file or parity file is a directory");
    
    LOG.info("Set modification time of parity to be a different number");
    dfs.setTimes(badParity, stat.getModificationTime() + 1, 0);
    mergeFile(badParity, source, "rs", checksums,
        "merge: source file and parity file doesn't have the same modification time");
    dfs.setTimes(badParity, stat.getModificationTime(), 0);
    dfs.setTimes(emptyFile, stat.getModificationTime(), 0);
    mergeFile(emptyFile, source, "rs", checksums, 
        "merge: parity file's replication doesn't match codec's parity replication");
    dfs.setReplication(emptyFile, (short)1);
    mergeFile(emptyFile, source, "rs", checksums, "merge: /empty is empty");
    mergeFile(badParity, emptyFile, "rs", checksums, "merge: /empty is empty");
    mergeFile(badParity, source, "rs", new int[5], "merge: checksum length ");
    mergeFile(badParity, source, "rs", checksums, "merge: expect parity blocks ");
    
    LOG.info("Hardlink the file to " + dest);
    dfs.hardLink(source, dest);
    mergeFile(emptyFile, source, "rs", checksums, 
        "merge: source file or parity file is hardlinked");
    mergeFile(dest, emptyFile, "rs", checksums, 
        "merge: source file or parity file is hardlinked");
  }
}