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

import com.google.common.collect.ImmutableList;
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.fs.XAttr;
import org.apache.hadoop.fs.XAttrSetFlag;
import org.apache.hadoop.hdfs.protocol.NSQuotaExceededException;
import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.apache.hadoop.hdfs.DFSTestUtil;
import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.hadoop.test.GenericTestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.google.common.collect.Lists;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Test {@link FSDirectory}, the in-memory namespace tree.
 */
public class TestFSDirectory {
  public static final Log LOG = LogFactory.getLog(TestFSDirectory.class);

  private static final long seed = 0;
  private static final short REPLICATION = 3;

  private final Path dir = new Path("/" + getClass().getSimpleName());
  
  private final Path sub1 = new Path(dir, "sub1");
  private final Path file1 = new Path(sub1, "file1");
  private final Path file2 = new Path(sub1, "file2");

  private final Path sub11 = new Path(sub1, "sub11");
  private final Path file3 = new Path(sub11, "file3");
  private final Path file5 = new Path(sub1, "z_file5");

  private final Path sub2 = new Path(dir, "sub2");
  private final Path file6 = new Path(sub2, "file6");

  private Configuration conf;
  private MiniDFSCluster cluster;
  private FSNamesystem fsn;
  private FSDirectory fsdir;

  private DistributedFileSystem hdfs;

  private static final int numGeneratedXAttrs = 256;
  private static final ImmutableList<XAttr> generatedXAttrs =
      ImmutableList.copyOf(generateXAttrs(numGeneratedXAttrs));

  @Before
  public void setUp() throws Exception {
    conf = new Configuration();
    conf.setInt(DFSConfigKeys.DFS_NAMENODE_MAX_XATTRS_PER_INODE_KEY, 2);
    cluster = new MiniDFSCluster.Builder(conf)
      .numDataNodes(REPLICATION)
      .build();
    cluster.waitActive();
    
    fsn = cluster.getNamesystem();
    fsdir = fsn.getFSDirectory();
    
    hdfs = cluster.getFileSystem();
    DFSTestUtil.createFile(hdfs, file1, 1024, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, file2, 1024, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, file3, 1024, REPLICATION, seed);

    DFSTestUtil.createFile(hdfs, file5, 1024, REPLICATION, seed);
    hdfs.mkdirs(sub2);

  }

  @After
  public void tearDown() throws Exception {
    if (cluster != null) {
      cluster.shutdown();
    }
  }

  /** Dump the tree, make some changes, and then dump the tree again. */
  @Test
  public void testDumpTree() throws Exception {
    final INode root = fsdir.getINode("/");

    LOG.info("Original tree");
    final StringBuffer b1 = root.dumpTreeRecursively();
    System.out.println("b1=" + b1);

    final BufferedReader in = new BufferedReader(new StringReader(b1.toString()));
    
    String line = in.readLine();
    checkClassName(line);

    for(; (line = in.readLine()) != null; ) {
      line = line.trim();
      if (!line.isEmpty() && !line.contains("snapshot")) {
        assertTrue("line=" + line,
            line.startsWith(INodeDirectory.DUMPTREE_LAST_ITEM)
                || line.startsWith(INodeDirectory.DUMPTREE_EXCEPT_LAST_ITEM)
        );
        checkClassName(line);
      }
    }
  }
  
  @Test
  public void testSkipQuotaCheck() throws Exception {
    try {
      // set quota. nsQuota of 1 means no files can be created
      //  under this directory.
      hdfs.setQuota(sub2, 1, Long.MAX_VALUE);

      // create a file
      try {
        // this should fail
        DFSTestUtil.createFile(hdfs, file6, 1024, REPLICATION, seed);
        throw new IOException("The create should have failed.");
      } catch (NSQuotaExceededException qe) {
        // ignored
      }
      // disable the quota check and retry. this should succeed.
      fsdir.disableQuotaChecks();
      DFSTestUtil.createFile(hdfs, file6, 1024, REPLICATION, seed);

      // trying again after re-enabling the check.
      hdfs.delete(file6, false); // cleanup
      fsdir.enableQuotaChecks();
      try {
        // this should fail
        DFSTestUtil.createFile(hdfs, file6, 1024, REPLICATION, seed);
        throw new IOException("The create should have failed.");
      } catch (NSQuotaExceededException qe) {
        // ignored
      }
    } finally {
      hdfs.delete(file6, false); // cleanup, in case the test failed in the middle.
      hdfs.setQuota(sub2, Long.MAX_VALUE, Long.MAX_VALUE);
    }
  }
  
  static void checkClassName(String line) {
    int i = line.lastIndexOf('(');
    int j = line.lastIndexOf('@');
    final String classname = line.substring(i+1, j);
    assertTrue(classname.startsWith(INodeFile.class.getSimpleName())
        || classname.startsWith(INodeDirectory.class.getSimpleName()));
  }
  
  @Test
  public void testINodeXAttrsLimit() throws Exception {
    List<XAttr> existingXAttrs = Lists.newArrayListWithCapacity(2);
    XAttr xAttr1 = (new XAttr.Builder()).setNameSpace(XAttr.NameSpace.USER).
        setName("a1").setValue(new byte[]{0x31, 0x32, 0x33}).build();
    XAttr xAttr2 = (new XAttr.Builder()).setNameSpace(XAttr.NameSpace.USER).
        setName("a2").setValue(new byte[]{0x31, 0x31, 0x31}).build();
    existingXAttrs.add(xAttr1);
    existingXAttrs.add(xAttr2);
    
    // Adding system and raw namespace xAttrs aren't affected by inode
    // xAttrs limit.
    XAttr newSystemXAttr = (new XAttr.Builder()).
        setNameSpace(XAttr.NameSpace.SYSTEM).setName("a3").
        setValue(new byte[]{0x33, 0x33, 0x33}).build();
    XAttr newRawXAttr = (new XAttr.Builder()).setNameSpace(XAttr.NameSpace.RAW).
        setName("a3").setValue(new byte[]{0x33, 0x33, 0x33}).build();
    List<XAttr> newXAttrs = Lists.newArrayListWithCapacity(2);
    newXAttrs.add(newSystemXAttr);
    newXAttrs.add(newRawXAttr);
    List<XAttr> xAttrs = FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs,
                                                     newXAttrs, EnumSet.of(
            XAttrSetFlag.CREATE, XAttrSetFlag.REPLACE));
    assertEquals(xAttrs.size(), 4);
    
    // Adding a trusted namespace xAttr, is affected by inode xAttrs limit.
    XAttr newXAttr1 = (new XAttr.Builder()).setNameSpace(
        XAttr.NameSpace.TRUSTED).setName("a4").
        setValue(new byte[]{0x34, 0x34, 0x34}).build();
    newXAttrs.set(0, newXAttr1);
    try {
      FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs, newXAttrs,
                                  EnumSet.of(XAttrSetFlag.CREATE,
                                             XAttrSetFlag.REPLACE));
      fail("Setting user visible xattr on inode should fail if " +
          "reaching limit.");
    } catch (IOException e) {
      GenericTestUtils.assertExceptionContains("Cannot add additional XAttr " +
          "to inode, would exceed limit", e);
    }
  }

  /**
   * Verify that the first <i>num</i> generatedXAttrs are present in
   * newXAttrs.
   */
  private static void verifyXAttrsPresent(List<XAttr> newXAttrs,
      final int num) {
    assertEquals("Unexpected number of XAttrs after multiset", num,
        newXAttrs.size());
    for (int i=0; i<num; i++) {
      XAttr search = generatedXAttrs.get(i);
      assertTrue("Did not find set XAttr " + search + " + after multiset",
          newXAttrs.contains(search));
    }
  }

  private static List<XAttr> generateXAttrs(final int numXAttrs) {
    List<XAttr> generatedXAttrs = Lists.newArrayListWithCapacity(numXAttrs);
    for (int i=0; i<numXAttrs; i++) {
      XAttr xAttr = (new XAttr.Builder())
          .setNameSpace(XAttr.NameSpace.SYSTEM)
          .setName("a" + i)
          .setValue(new byte[] { (byte) i, (byte) (i + 1), (byte) (i + 2) })
          .build();
      generatedXAttrs.add(xAttr);
    }
    return generatedXAttrs;
  }

  /**
   * Test setting and removing multiple xattrs via single operations
   */
  @Test(timeout=300000)
  public void testXAttrMultiSetRemove() throws Exception {
    List<XAttr> existingXAttrs = Lists.newArrayListWithCapacity(0);

    // Keep adding a random number of xattrs and verifying until exhausted
    final Random rand = new Random(0xFEEDA);
    int numExpectedXAttrs = 0;
    while (numExpectedXAttrs < numGeneratedXAttrs) {
      LOG.info("Currently have " + numExpectedXAttrs + " xattrs");
      final int numToAdd = rand.nextInt(5)+1;

      List<XAttr> toAdd = Lists.newArrayListWithCapacity(numToAdd);
      for (int i = 0; i < numToAdd; i++) {
        if (numExpectedXAttrs >= numGeneratedXAttrs) {
          break;
        }
        toAdd.add(generatedXAttrs.get(numExpectedXAttrs));
        numExpectedXAttrs++;
      }
      LOG.info("Attempting to add " + toAdd.size() + " XAttrs");
      for (int i = 0; i < toAdd.size(); i++) {
        LOG.info("Will add XAttr " + toAdd.get(i));
      }
      List<XAttr> newXAttrs = FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs,
                                                          toAdd, EnumSet.of(
              XAttrSetFlag.CREATE));
      verifyXAttrsPresent(newXAttrs, numExpectedXAttrs);
      existingXAttrs = newXAttrs;
    }

    // Keep removing a random number of xattrs and verifying until all gone
    while (numExpectedXAttrs > 0) {
      LOG.info("Currently have " + numExpectedXAttrs + " xattrs");
      final int numToRemove = rand.nextInt(5)+1;
      List<XAttr> toRemove = Lists.newArrayListWithCapacity(numToRemove);
      for (int i = 0; i < numToRemove; i++) {
        if (numExpectedXAttrs == 0) {
          break;
        }
        toRemove.add(generatedXAttrs.get(numExpectedXAttrs-1));
        numExpectedXAttrs--;
      }
      final int expectedNumToRemove = toRemove.size();
      LOG.info("Attempting to remove " + expectedNumToRemove + " XAttrs");
      List<XAttr> removedXAttrs = Lists.newArrayList();
      List<XAttr> newXAttrs = FSDirXAttrOp.filterINodeXAttrs(existingXAttrs,
                                                             toRemove,
                                                             removedXAttrs);
      assertEquals("Unexpected number of removed XAttrs",
          expectedNumToRemove, removedXAttrs.size());
      verifyXAttrsPresent(newXAttrs, numExpectedXAttrs);
      existingXAttrs = newXAttrs;
    }
  }

  @Test(timeout=300000)
  public void testXAttrMultiAddRemoveErrors() throws Exception {

    // Test that the same XAttr can not be multiset twice
    List<XAttr> existingXAttrs = Lists.newArrayList();
    List<XAttr> toAdd = Lists.newArrayList();
    toAdd.add(generatedXAttrs.get(0));
    toAdd.add(generatedXAttrs.get(1));
    toAdd.add(generatedXAttrs.get(2));
    toAdd.add(generatedXAttrs.get(0));
    try {
      FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs, toAdd,
                                  EnumSet.of(XAttrSetFlag.CREATE));
      fail("Specified the same xattr to be set twice");
    } catch (IOException e) {
      GenericTestUtils.assertExceptionContains("Cannot specify the same " +
          "XAttr to be set", e);
    }

    // Test that CREATE and REPLACE flags are obeyed
    toAdd.remove(generatedXAttrs.get(0));
    existingXAttrs.add(generatedXAttrs.get(0));
    try {
      FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs, toAdd,
                                  EnumSet.of(XAttrSetFlag.CREATE));
      fail("Set XAttr that is already set without REPLACE flag");
    } catch (IOException e) {
      GenericTestUtils.assertExceptionContains("already exists", e);
    }
    try {
      FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs, toAdd,
                                  EnumSet.of(XAttrSetFlag.REPLACE));
      fail("Set XAttr that does not exist without the CREATE flag");
    } catch (IOException e) {
      GenericTestUtils.assertExceptionContains("does not exist", e);
    }

    // Sanity test for CREATE
    toAdd.remove(generatedXAttrs.get(0));
    List<XAttr> newXAttrs = FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs,
                                                        toAdd, EnumSet.of(
            XAttrSetFlag.CREATE));
    assertEquals("Unexpected toAdd size", 2, toAdd.size());
    for (XAttr x : toAdd) {
      assertTrue("Did not find added XAttr " + x, newXAttrs.contains(x));
    }
    existingXAttrs = newXAttrs;

    // Sanity test for REPLACE
    toAdd = Lists.newArrayList();
    for (int i=0; i<3; i++) {
      XAttr xAttr = (new XAttr.Builder())
          .setNameSpace(XAttr.NameSpace.SYSTEM)
          .setName("a" + i)
          .setValue(new byte[] { (byte) (i*2) })
          .build();
      toAdd.add(xAttr);
    }
    newXAttrs = FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs, toAdd,
                                            EnumSet.of(XAttrSetFlag.REPLACE));
    assertEquals("Unexpected number of new XAttrs", 3, newXAttrs.size());
    for (int i=0; i<3; i++) {
      assertArrayEquals("Unexpected XAttr value",
          new byte[] {(byte)(i*2)}, newXAttrs.get(i).getValue());
    }
    existingXAttrs = newXAttrs;

    // Sanity test for CREATE+REPLACE
    toAdd = Lists.newArrayList();
    for (int i=0; i<4; i++) {
      toAdd.add(generatedXAttrs.get(i));
    }
    newXAttrs = FSDirXAttrOp.setINodeXAttrs(fsdir, existingXAttrs, toAdd,
                                            EnumSet.of(XAttrSetFlag.CREATE,
                                                       XAttrSetFlag.REPLACE));
    verifyXAttrsPresent(newXAttrs, 4);
  }
}
