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

import java.io.*;
import java.net.URI;
import java.util.EnumSet;
import org.apache.hadoop.fs.FileContext;
import org.apache.hadoop.fs.Options.CreateOpts;
import org.apache.hadoop.fs.Options.Rename;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.fs.CreateFlag;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataOutputStream;
import static org.apache.hadoop.fs.FileContextTestHelper.*;

import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;

/**
 * Test symbolic links using FileContext.
 */
public abstract class FileContextSymlinkBaseTest {
  static final long seed = 0xDEADBEEFL;
  static final int  blockSize =  8192;
  static final int  fileSize  = 16384;
 
  protected static FileContext fc;

  abstract protected String getScheme();
  abstract protected String testBaseDir1() throws IOException;
  abstract protected String testBaseDir2() throws IOException;
  abstract protected URI testURI();

  protected IOException unwrapException(IOException e) {
    return e;
  }

  protected static void createAndWriteFile(FileContext fc, Path p) 
      throws IOException {
    createFile(fc, p, fileSize / blockSize,
        CreateOpts.createParent(),
        CreateOpts.repFac((short) 1),
        CreateOpts.blockSize(blockSize));
  }

  protected static void createAndWriteFile(Path p) throws IOException {
    createAndWriteFile(fc, p);
  }

  protected static void readFile(Path p) throws IOException {
    FileContextTestHelper.readFile(fc, p, fileSize);
  }

  protected static void readFile(FileContext fc, Path p) throws IOException {
    FileContextTestHelper.readFile(fc, p, fileSize);
  }

  protected static void appendToFile(Path p) throws IOException {
    FileContextTestHelper.appendToFile(fc, p, fileSize / blockSize, 
        CreateOpts.blockSize(blockSize));
  }

  @Before
  public void setUp() throws Exception {
    fc.mkdir(new Path(testBaseDir1()), FileContext.DEFAULT_PERM, true);
    fc.mkdir(new Path(testBaseDir2()), FileContext.DEFAULT_PERM, true);
  }

  @After
  public void tearDown() throws Exception { 
    fc.delete(new Path(testBaseDir1()), true);
    fc.delete(new Path(testBaseDir2()), true);
  } 

  @Test
  /** The root is not a symlink */
  public void testStatRoot() throws IOException {
    assertFalse(fc.getFileLinkStatus(new Path("/")).isSymlink());    
  }

  @Test
  /** Test setWorkingDirectory not resolves symlinks */
  public void testSetWDNotResolvesLinks() throws IOException {
    Path dir       = new Path(testBaseDir1());
    Path linkToDir = new Path(testBaseDir1()+"/link");
    fc.createSymlink(dir, linkToDir, false);
    fc.setWorkingDirectory(linkToDir);
    assertEquals(linkToDir.getName(), fc.getWorkingDirectory().getName());
  }

  @Test
  /** Test create a dangling link */
  public void testCreateDanglingLink() throws IOException {
    Path file = new Path("/noSuchFile");
    Path link = new Path(testBaseDir1()+"/link");    
    fc.createSymlink(file, link, false);
    try {
      fc.getFileStatus(link);
      fail("Got file status of non-existant file");
    } catch (FileNotFoundException f) {
      // Expected
    }
    fc.delete(link, false);
  } 

  @Test
  /** Test create a link to null and empty path */
  public void testCreateLinkToNullEmpty() throws IOException {
    Path link = new Path(testBaseDir1()+"/link");
    try {
      fc.createSymlink(null, link, false);
      fail("Can't create symlink to null");
    } catch (java.lang.NullPointerException e) {
      // Expected, create* with null yields NPEs
    }
    try {
      fc.createSymlink(new Path(""), link, false);
      fail("Can't create symlink to empty string");
    } catch (java.lang.IllegalArgumentException e) {
      // Expected, Path("") is invalid
    }
  } 
    
  @Test
  /** Create a link with createParent set */
  public void testCreateLinkCanCreateParent() throws IOException {
    Path file = new Path(testBaseDir1()+"/file");
    Path link = new Path(testBaseDir2()+"/linkToFile");
    createAndWriteFile(file);
    fc.delete(new Path(testBaseDir2()), true);
    try {
      fc.createSymlink(file, link, false);
      fail("Created link without first creating parent dir");
    } catch (IOException x) {
      // Expected. Need to create testBaseDir2() first.
    }
    assertFalse(exists(fc, new Path(testBaseDir2())));
    fc.createSymlink(file, link, true);
    readFile(link);
  }

  @Test
  /** Try to create a directory given a path that refers to a symlink */
  public void testMkdirExistingLink() throws IOException {
    Path dir  = new Path(testBaseDir1()+"/link");
    fc.createSymlink(new Path("/doesNotExist"), dir, false);
    try {
      fc.mkdir(dir, FileContext.DEFAULT_PERM, false);
      fail("Created a dir where a symlink exists");
    } catch (FileAlreadyExistsException e) {
      // Expected. The symlink already exists.
    } catch (IOException e) {
      // LocalFs just throws an IOException
      assertEquals("file", getScheme());
    }
  }

  @Test
  /** Try to create a file with parent that is a dangling link */
  public void testCreateFileViaDanglingLinkParent() throws IOException {
    Path dir  = new Path(testBaseDir1()+"/dangling");
    Path file = new Path(testBaseDir1()+"/dangling/file");
    fc.createSymlink(new Path("/doesNotExist"), dir, false);
    FSDataOutputStream out;
    try {
      out = fc.create(file, EnumSet.of(CreateFlag.CREATE), 
                      CreateOpts.repFac((short) 1),
                      CreateOpts.blockSize(blockSize));
      out.close();
      fail("Created a link with dangling link parent");
    } catch (FileNotFoundException e) {
      // Expected. The parent is dangling.
    }
  }

  @Test
  /** Delete a link */
  public void testDeleteLink() throws IOException {
    Path file = new Path(testBaseDir1()+"/file");
    Path link = new Path(testBaseDir1()+"/linkToFile");    
    createAndWriteFile(file);  
    fc.createSymlink(file, link, false);
    readFile(link);
    fc.delete(link, false);
    try {
      readFile(link);
      fail("Symlink should have been deleted");
    } catch (IOException x) {
      // Expected
    }
    // If we deleted the link we can put it back
    fc.createSymlink(file, link, false);    
  }
  
  @Test
  /** Ensure open resolves symlinks */
  public void testOpenResolvesLinks() throws IOException {
    Path file = new Path(testBaseDir1()+"/noSuchFile");
    Path link = new Path(testBaseDir1()+"/link");
    fc.createSymlink(file, link, false);
    try {
      fc.open(link);
      fail("link target does not exist");
    } catch (FileNotFoundException x) {
      // Expected
    }
    fc.delete(link, false);
  }

  @Test
  /** Stat a link to a file */
  public void testStatLinkToFile() throws IOException {
    Path file = new Path(testBaseDir1()+"/file");
    Path linkToFile = new Path(testBaseDir1()+"/linkToFile");    
    createAndWriteFile(file);
    fc.createSymlink(file, linkToFile, false);
    assertFalse(fc.getFileLinkStatus(linkToFile).isDirectory());
    assertTrue(isSymlink(fc, linkToFile));
    assertTrue(isFile(fc, linkToFile));
    assertFalse(isDir(fc, linkToFile));
    assertEquals(file.toUri().getPath(), 
                 fc.getLinkTarget(linkToFile).toString());
    // The local file system does not fully resolve the link
    // when obtaining the file status
    if (!"file".equals(getScheme())) {
      assertEquals(fc.getFileStatus(file), fc.getFileStatus(linkToFile));
      assertEquals(fc.makeQualified(file),
                   fc.getFileStatus(linkToFile).getPath());
      assertEquals(fc.makeQualified(linkToFile),
                   fc.getFileLinkStatus(linkToFile).getPath());
    }
  }

  @Test
  /** Stat a relative link to a file */
  public void testStatRelLinkToFile() throws IOException {
    assumeTrue(!"file".equals(getScheme()));
    Path baseDir    = new Path(testBaseDir1());
    Path file       = new Path(testBaseDir1(), "file");
    Path linkToFile = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(new Path("file"), linkToFile, false);
    assertEquals(fc.getFileStatus(file), fc.getFileStatus(linkToFile));
    assertEquals(fc.makeQualified(file),
                 fc.getFileStatus(linkToFile).getPath());
    assertEquals(fc.makeQualified(linkToFile),
                 fc.getFileLinkStatus(linkToFile).getPath());
  }

  @Test
  /** Stat a link to a directory */
  public void testStatLinkToDir() throws IOException {
    Path dir  = new Path(testBaseDir1());
    Path linkToDir = new Path(testBaseDir1()+"/linkToDir");
    fc.createSymlink(dir, linkToDir, false);

    assertFalse(fc.getFileStatus(linkToDir).isSymlink());
    assertTrue(isDir(fc, linkToDir));
    assertFalse(fc.getFileLinkStatus(linkToDir).isDirectory());
    assertTrue(fc.getFileLinkStatus(linkToDir).isSymlink());

    assertFalse(isFile(fc, linkToDir));
    assertTrue(isDir(fc, linkToDir));

    assertEquals(dir.toUri().getPath(), 
                 fc.getLinkTarget(linkToDir).toString());
  }

  @Test
  /** Stat a dangling link */
  public void testStatDanglingLink() throws IOException {
    Path file = new Path("/noSuchFile");
    Path link = new Path(testBaseDir1()+"/link");    
    fc.createSymlink(file, link, false);
    assertFalse(fc.getFileLinkStatus(link).isDirectory());
    assertTrue(fc.getFileLinkStatus(link).isSymlink());
  }
  
  @Test
  /** Stat a non-existant file */
  public void testStatNonExistantFiles() throws IOException {
    Path fileAbs = new Path("/doesNotExist");
    try {
      fc.getFileLinkStatus(fileAbs);
      fail("Got FileStatus for non-existant file");
    } catch (FileNotFoundException f) {
      // Expected
    }
    try {
      fc.getLinkTarget(fileAbs);
      fail("Got link target for non-existant file");
    } catch (FileNotFoundException f) {
      // Expected
    }
  }

  @Test
  /** Test stat'ing a regular file and directory */
  public void testStatNonLinks() throws IOException {
    Path dir   = new Path(testBaseDir1());
    Path file  = new Path(testBaseDir1()+"/file");
    createAndWriteFile(file);
    try {
      fc.getLinkTarget(dir);
      fail("Lstat'd a non-symlink");
    } catch (IOException e) {
      // Expected.
    }
    try {
      fc.getLinkTarget(file);
      fail("Lstat'd a non-symlink");
    } catch (IOException e) {
      // Expected.
    }
  }
  
  @Test
  /** Test links that link to each other */
  public void testRecursiveLinks() throws IOException {
    Path link1 = new Path(testBaseDir1()+"/link1");
    Path link2 = new Path(testBaseDir1()+"/link2");
    fc.createSymlink(link1, link2, false);
    fc.createSymlink(link2, link1, false);
    try {
      readFile(link1);
      fail("Read recursive link");
    } catch (FileNotFoundException f) {
      // LocalFs throws sub class of IOException, since File.exists 
      // returns false for a link to link. 
    } catch (IOException x) {
      assertEquals("Possible cyclic loop while following symbolic link "+
                   link1.toString(), x.getMessage());
    }    
  }

  /* Assert that the given link to a file behaves as expected. */
  private void checkLink(Path linkAbs, Path expectedTarget, Path targetQual) 
      throws IOException { 
    Path dir = new Path(testBaseDir1());
    // isFile/Directory
    assertTrue(isFile(fc, linkAbs));
    assertFalse(isDir(fc, linkAbs));

    // Check getFileStatus 
    assertFalse(fc.getFileStatus(linkAbs).isSymlink());
    assertFalse(fc.getFileStatus(linkAbs).isDirectory());
    assertEquals(fileSize, fc.getFileStatus(linkAbs).getLen());

    // Check getFileLinkStatus
    assertTrue(isSymlink(fc, linkAbs));
    assertFalse(fc.getFileLinkStatus(linkAbs).isDirectory());

    // Check getSymlink always returns a qualified target, except
    // when partially qualified paths are used (see tests below).
    assertEquals(targetQual.toString(), 
        fc.getFileLinkStatus(linkAbs).getSymlink().toString());
    assertEquals(targetQual, fc.getFileLinkStatus(linkAbs).getSymlink());
    // Check that the target is qualified using the file system of the 
    // path used to access the link (if the link target was not specified 
    // fully qualified, in that case we use the link target verbatim).
    if (!"file".equals(getScheme())) {
      FileContext localFc = FileContext.getLocalFSFileContext();
      Path linkQual = new Path(testURI().toString(), linkAbs);
      assertEquals(targetQual, 
                   localFc.getFileLinkStatus(linkQual).getSymlink());
    }
    
    // Check getLinkTarget
    assertEquals(expectedTarget, fc.getLinkTarget(linkAbs));
    
    // Now read using all path types..
    fc.setWorkingDirectory(dir);    
    readFile(new Path("linkToFile"));
    readFile(linkAbs);
    // And fully qualified.. (NB: for local fs this is partially qualified)
    readFile(new Path(testURI().toString(), linkAbs));
    // And partially qualified..
    boolean failureExpected = "file".equals(getScheme()) ? false : true;
    try {
      readFile(new Path(getScheme()+"://"+testBaseDir1()+"/linkToFile"));
      assertFalse(failureExpected);
    } catch (Exception e) {
      assertTrue(failureExpected);
    }
    
    // Now read using a different file context (for HDFS at least)
    if (!"file".equals(getScheme())) {
      FileContext localFc = FileContext.getLocalFSFileContext();
      readFile(localFc, new Path(testURI().toString(), linkAbs));
    }
  }
  
  @Test
  /** Test creating a symlink using relative paths */
  public void testCreateLinkUsingRelPaths() throws IOException {
    Path fileAbs = new Path(testBaseDir1(), "file");
    Path linkAbs = new Path(testBaseDir1(), "linkToFile");
    Path schemeAuth = new Path(testURI().toString()); 
    Path fileQual = new Path(schemeAuth, testBaseDir1()+"/file");
    createAndWriteFile(fileAbs);
    
    fc.setWorkingDirectory(new Path(testBaseDir1()));
    fc.createSymlink(new Path("file"), new Path("linkToFile"), false);
    checkLink(linkAbs, new Path("file"), fileQual);
    
    // Now rename the link's parent. Because the target was specified 
    // with a relative path the link should still resolve.
    Path dir1        = new Path(testBaseDir1());
    Path dir2        = new Path(testBaseDir2());
    Path linkViaDir2 = new Path(testBaseDir2(), "linkToFile");
    Path fileViaDir2 = new Path(schemeAuth, testBaseDir2()+"/file");
    fc.rename(dir1, dir2, Rename.OVERWRITE);
    assertEquals(fileViaDir2, fc.getFileLinkStatus(linkViaDir2).getSymlink());
    readFile(linkViaDir2);
  }

  @Test
  /** Test creating a symlink using absolute paths */
  public void testCreateLinkUsingAbsPaths() throws IOException {
    Path fileAbs = new Path(testBaseDir1()+"/file");
    Path linkAbs = new Path(testBaseDir1()+"/linkToFile");
    Path schemeAuth = new Path(testURI().toString()); 
    Path fileQual = new Path(schemeAuth, testBaseDir1()+"/file");
    createAndWriteFile(fileAbs);

    fc.createSymlink(fileAbs, linkAbs, false);
    checkLink(linkAbs, fileAbs, fileQual);

    // Now rename the link's parent. The target doesn't change and
    // now no longer exists so accessing the link should fail.
    Path dir1        = new Path(testBaseDir1());
    Path dir2        = new Path(testBaseDir2());
    Path linkViaDir2 = new Path(testBaseDir2(), "linkToFile");
    fc.rename(dir1, dir2, Rename.OVERWRITE);
    assertEquals(fileQual, fc.getFileLinkStatus(linkViaDir2).getSymlink());    
    try {
      readFile(linkViaDir2);
      fail("The target should not exist");
    } catch (FileNotFoundException x) {
      // Expected
    }
  } 
  
  @Test
  /** 
   * Test creating a symlink using fully and partially qualified paths.
   * NB: For local fs this actually tests partially qualified paths,
   * as they don't support fully qualified paths.
   */
  public void testCreateLinkUsingFullyQualPaths() throws IOException {
    Path fileAbs  = new Path(testBaseDir1(), "file");
    Path linkAbs  = new Path(testBaseDir1(), "linkToFile");
    Path fileQual = new Path(testURI().toString(), fileAbs);
    Path linkQual = new Path(testURI().toString(), linkAbs);
    createAndWriteFile(fileAbs);
    
    fc.createSymlink(fileQual, linkQual, false);
    checkLink(linkAbs, 
              "file".equals(getScheme()) ? fileAbs : fileQual, 
              fileQual);
    
    // Now rename the link's parent. The target doesn't change and
    // now no longer exists so accessing the link should fail.
    Path dir1        = new Path(testBaseDir1());
    Path dir2        = new Path(testBaseDir2());
    Path linkViaDir2 = new Path(testBaseDir2(), "linkToFile");
    fc.rename(dir1, dir2, Rename.OVERWRITE);    
    assertEquals(fileQual, fc.getFileLinkStatus(linkViaDir2).getSymlink());    
    try {
      readFile(linkViaDir2);
      fail("The target should not exist");
    } catch (FileNotFoundException x) {
      // Expected
    }
  } 
    
  @Test
  /** 
   * Test creating a symlink using partially qualified paths, ie a scheme 
   * but no authority and vice versa. We just test link targets here since
   * creating using a partially qualified path is file system specific.
   */
  public void testCreateLinkUsingPartQualPath1() throws IOException {
    // Partially qualified paths are covered for local file systems
    // in the previous test.
    assumeTrue(!"file".equals(getScheme()));
    Path schemeAuth   = new Path(testURI().toString());
    Path fileWoHost   = new Path(getScheme()+"://"+testBaseDir1()+"/file");
    Path link         = new Path(testBaseDir1()+"/linkToFile");
    Path linkQual     = new Path(schemeAuth, testBaseDir1()+"/linkToFile");
    FileContext localFc = FileContext.getLocalFSFileContext();

    fc.createSymlink(fileWoHost, link, false);
    // Partially qualified path is stored
    assertEquals(fileWoHost, fc.getLinkTarget(linkQual));    
    // NB: We do not add an authority
    assertEquals(fileWoHost.toString(),
      fc.getFileLinkStatus(link).getSymlink().toString());
    assertEquals(fileWoHost.toString(),
      fc.getFileLinkStatus(linkQual).getSymlink().toString());
    // Ditto even from another file system
    assertEquals(fileWoHost.toString(),
      localFc.getFileLinkStatus(linkQual).getSymlink().toString());
    // Same as if we accessed a partially qualified path directly
    try { 
      readFile(link);
      fail("DFS requires URIs with schemes have an authority");
    } catch (java.lang.RuntimeException e) {
      // Expected
    }
  }

  @Test
  /** Same as above but vice versa (authority but no scheme) */
  public void testCreateLinkUsingPartQualPath2() throws IOException {
    Path link         = new Path(testBaseDir1(), "linkToFile");
    Path fileWoScheme = new Path("//"+testURI().getAuthority()+ 
                                 testBaseDir1()+"/file");
    if ("file".equals(getScheme())) {
      return;
    }
    fc.createSymlink(fileWoScheme, link, false);
    assertEquals(fileWoScheme, fc.getLinkTarget(link));
    assertEquals(fileWoScheme.toString(),
      fc.getFileLinkStatus(link).getSymlink().toString());
    try {
      readFile(link);
      fail("Accessed a file with w/o scheme");
    } catch (IOException e) {
      // Expected      
      assertEquals("No AbstractFileSystem for scheme: null", e.getMessage());
    }
  }

  @Test
  /** Lstat and readlink on a normal file and directory */
  public void testLinkStatusAndTargetWithNonLink() throws IOException {
    Path schemeAuth = new Path(testURI().toString());
    Path dir        = new Path(testBaseDir1());
    Path dirQual    = new Path(schemeAuth, dir.toString());
    Path file       = new Path(testBaseDir1(), "file");
    Path fileQual   = new Path(schemeAuth, file.toString());
    createAndWriteFile(file);
    assertEquals(fc.getFileStatus(file), fc.getFileLinkStatus(file));
    assertEquals(fc.getFileStatus(dir), fc.getFileLinkStatus(dir));
    try {
      fc.getLinkTarget(file);
      fail("Get link target on non-link should throw an IOException");
    } catch (IOException x) {
      assertEquals("Path "+fileQual+" is not a symbolic link", x.getMessage());
    }
    try {
      fc.getLinkTarget(dir);
      fail("Get link target on non-link should throw an IOException");
    } catch (IOException x) {
      assertEquals("Path "+dirQual+" is not a symbolic link", x.getMessage());
    }    
  }

  @Test
  /** Test create symlink to a directory */
  public void testCreateLinkToDirectory() throws IOException {
    Path dir1      = new Path(testBaseDir1());
    Path file      = new Path(testBaseDir1(), "file");
    Path linkToDir = new Path(testBaseDir2(), "linkToDir");
    createAndWriteFile(file);
    fc.createSymlink(dir1, linkToDir, false);
    assertFalse(isFile(fc, linkToDir));
    assertTrue(isDir(fc, linkToDir)); 
    assertTrue(fc.getFileStatus(linkToDir).isDirectory());
    assertTrue(fc.getFileLinkStatus(linkToDir).isSymlink());
  }
  
  @Test
  /** Test create and remove a file through a symlink */
  public void testCreateFileViaSymlink() throws IOException {
    Path dir         = new Path(testBaseDir1());
    Path linkToDir   = new Path(testBaseDir2(), "linkToDir");
    Path fileViaLink = new Path(linkToDir, "file");
    fc.createSymlink(dir, linkToDir, false);
    createAndWriteFile(fileViaLink);
    assertTrue(isFile(fc, fileViaLink));
    assertFalse(isDir(fc, fileViaLink));
    assertFalse(fc.getFileLinkStatus(fileViaLink).isSymlink());
    assertFalse(fc.getFileStatus(fileViaLink).isDirectory());
    readFile(fileViaLink);
    fc.delete(fileViaLink, true);
    assertFalse(exists(fc, fileViaLink));
  }
  
  @Test
  /** Test make and delete directory through a symlink */
  public void testCreateDirViaSymlink() throws IOException {
    Path dir1          = new Path(testBaseDir1());
    Path subDir        = new Path(testBaseDir1(), "subDir");
    Path linkToDir     = new Path(testBaseDir2(), "linkToDir");
    Path subDirViaLink = new Path(linkToDir, "subDir");
    fc.createSymlink(dir1, linkToDir, false);
    fc.mkdir(subDirViaLink, FileContext.DEFAULT_PERM, true);
    assertTrue(isDir(fc, subDirViaLink));
    fc.delete(subDirViaLink, false);
    assertFalse(exists(fc, subDirViaLink));
    assertFalse(exists(fc, subDir));
  }

  @Test
  /** Create symlink through a symlink */
  public void testCreateLinkViaLink() throws IOException {
    Path dir1        = new Path(testBaseDir1());
    Path file        = new Path(testBaseDir1(), "file");
    Path linkToDir   = new Path(testBaseDir2(), "linkToDir");
    Path fileViaLink = new Path(linkToDir, "file");
    Path linkToFile  = new Path(linkToDir, "linkToFile");
    /*
     * /b2/linkToDir            -> /b1
     * /b2/linkToDir/linkToFile -> /b2/linkToDir/file
     */
    createAndWriteFile(file);
    fc.createSymlink(dir1, linkToDir, false);
    fc.createSymlink(fileViaLink, linkToFile, false);
    assertTrue(isFile(fc, linkToFile));
    assertTrue(fc.getFileLinkStatus(linkToFile).isSymlink());
    readFile(linkToFile);
    assertEquals(fileSize, fc.getFileStatus(linkToFile).getLen());
    assertEquals(fileViaLink, fc.getLinkTarget(linkToFile));
  }

  @Test
  /** Test create symlink to a directory */
  public void testListStatusUsingLink() throws IOException {
    Path file  = new Path(testBaseDir1(), "file");
    Path link  = new Path(testBaseDir1(), "link");
    createAndWriteFile(file);
    fc.createSymlink(new Path(testBaseDir1()), link, false);
    // The size of the result is file system dependent, Hdfs is 2 (file 
    // and link) and LocalFs is 3 (file, link, file crc).
    FileStatus[] stats = fc.util().listStatus(link);
    assertTrue(stats.length == 2 || stats.length == 3);
    RemoteIterator<FileStatus> statsItor = fc.listStatus(link);
    int dirLen = 0;
    while(statsItor.hasNext()) {
      statsItor.next();
      dirLen++;
    }
    assertTrue(dirLen == 2 || dirLen == 3);
  }
  
  @Test
  /** Test create symlink using the same path */
  public void testCreateLinkTwice() throws IOException {
    Path file = new Path(testBaseDir1(), "file");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(file, link, false);
    try {
      fc.createSymlink(file, link, false);
      fail("link already exists");
    } catch (IOException x) {
      // Expected
    }
  } 
  
  @Test
  /** Test access via a symlink to a symlink */
  public void testCreateLinkToLink() throws IOException {
    Path dir1        = new Path(testBaseDir1());
    Path file        = new Path(testBaseDir1(), "file");
    Path linkToDir   = new Path(testBaseDir2(), "linkToDir");
    Path linkToLink  = new Path(testBaseDir2(), "linkToLink");
    Path fileViaLink = new Path(testBaseDir2(), "linkToLink/file");
    createAndWriteFile(file);
    fc.createSymlink(dir1, linkToDir, false);
    fc.createSymlink(linkToDir, linkToLink, false);
    assertTrue(isFile(fc, fileViaLink));
    assertFalse(isDir(fc, fileViaLink));
    assertFalse(fc.getFileLinkStatus(fileViaLink).isSymlink());
    assertFalse(fc.getFileStatus(fileViaLink).isDirectory());
    readFile(fileViaLink);
  }

  @Test
  /** Can not create a file with path that refers to a symlink */
  public void testCreateFileDirExistingLink() throws IOException {
    Path file = new Path(testBaseDir1(), "file");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(file, link, false);
    try {
      createAndWriteFile(link);
      fail("link already exists");
    } catch (IOException x) {
      // Expected
    }
    try {
      fc.mkdir(link, FsPermission.getDefault(), false);
      fail("link already exists");
    } catch (IOException x) {
      // Expected
    }    
  } 

  @Test
  /** Test deleting and recreating a symlink */
  public void testUseLinkAferDeleteLink() throws IOException {
    Path file = new Path(testBaseDir1(), "file");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(file, link, false);
    fc.delete(link, false);
    try {
      readFile(link);        
      fail("link was deleted");
    } catch (IOException x) {
      // Expected
    }
    readFile(file);
    fc.createSymlink(file, link, false);
    readFile(link);    
  } 
  
  
  @Test
  /** Test create symlink to . */
  public void testCreateLinkToDot() throws IOException {
    Path dir  = new Path(testBaseDir1());
    Path file = new Path(testBaseDir1(), "file");    
    Path link = new Path(testBaseDir1(), "linkToDot");
    createAndWriteFile(file);    
    fc.setWorkingDirectory(dir);
    try {
      fc.createSymlink(new Path("."), link, false);
      fail("Created symlink to dot");
    } catch (IOException x) {
      // Expected. Path(".") resolves to "" because URI normalizes
      // the dot away and AbstractFileSystem considers "" invalid.  
    }
  }

  @Test
  /** Test create symlink to .. */
  public void testCreateLinkToDotDot() throws IOException {
    Path file        = new Path(testBaseDir1(), "test/file");
    Path dotDot      = new Path(testBaseDir1(), "test/..");
    Path linkToDir   = new Path(testBaseDir2(), "linkToDir");
    Path fileViaLink = new Path(linkToDir,      "test/file");
    // Symlink to .. is not a problem since the .. is squashed early
    assertEquals(testBaseDir1(), dotDot.toString());
    createAndWriteFile(file);
    fc.createSymlink(dotDot, linkToDir, false);
    readFile(fileViaLink);
    assertEquals(fileSize, fc.getFileStatus(fileViaLink).getLen());    
  }

  @Test
  /** Test create symlink to ../file */
  public void testCreateLinkToDotDotPrefix() throws IOException {
    Path file = new Path(testBaseDir1(), "file");
    Path dir  = new Path(testBaseDir1(), "test");
    Path link = new Path(testBaseDir1(), "test/link");
    createAndWriteFile(file);
    fc.mkdir(dir, FsPermission.getDefault(), false);
    fc.setWorkingDirectory(dir);
    fc.createSymlink(new Path("../file"), link, false);
    readFile(link);
    assertEquals(new Path("../file"), fc.getLinkTarget(link));
  }
  
  @Test
  /** Test rename file using a path that contains a symlink. The rename should 
   * work as if the path did not contain a symlink */
  public void testRenameFileViaSymlink() throws IOException {
    Path dir            = new Path(testBaseDir1());
    Path file           = new Path(testBaseDir1(), "file");
    Path linkToDir      = new Path(testBaseDir2(), "linkToDir");
    Path fileViaLink    = new Path(linkToDir, "file");
    Path fileNewViaLink = new Path(linkToDir, "fileNew");
    createAndWriteFile(file);
    fc.createSymlink(dir, linkToDir, false);
    fc.rename(fileViaLink, fileNewViaLink);
    assertFalse(exists(fc, fileViaLink));
    assertFalse(exists(fc, file));
    assertTrue(exists(fc, fileNewViaLink));
  }

  @Test
  /** Test rename a file through a symlink but this time only the 
   * destination path has an intermediate symlink. The rename should work 
   * as if the path did not contain a symlink */
  public void testRenameFileToDestViaSymlink() throws IOException {
    Path dir       = new Path(testBaseDir1());
    Path file      = new Path(testBaseDir1(), "file");
    Path linkToDir = new Path(testBaseDir2(), "linkToDir");
    Path subDir    = new Path(linkToDir, "subDir");
    createAndWriteFile(file);
    fc.createSymlink(dir, linkToDir, false);
    fc.mkdir(subDir, FileContext.DEFAULT_PERM, false);
    try {
      fc.rename(file, subDir);
      fail("Renamed file to a directory");
    } catch (IOException e) {
      // Expected. Both must be directories.
      assertTrue(unwrapException(e) instanceof IOException);
    }
    assertTrue(exists(fc, file));
  }
  
  @Test
  /** Similar tests as the previous ones but rename a directory */
  public void testRenameDirViaSymlink() throws IOException {
    Path baseDir       = new Path(testBaseDir1());
    Path dir           = new Path(baseDir, "dir");
    Path linkToDir     = new Path(testBaseDir2(), "linkToDir");
    Path dirViaLink    = new Path(linkToDir, "dir");
    Path dirNewViaLink = new Path(linkToDir, "dirNew");
    fc.mkdir(dir, FileContext.DEFAULT_PERM, false);
    fc.createSymlink(baseDir, linkToDir, false);
    assertTrue(exists(fc, dirViaLink));
    fc.rename(dirViaLink, dirNewViaLink);
    assertFalse(exists(fc, dirViaLink));
    assertFalse(exists(fc, dir));
    assertTrue(exists(fc, dirNewViaLink));
  }
    
  @Test
  /** Similar tests as the previous ones but rename a symlink */  
  public void testRenameSymlinkViaSymlink() throws IOException {
    Path baseDir        = new Path(testBaseDir1());
    Path file           = new Path(testBaseDir1(), "file");
    Path link           = new Path(testBaseDir1(), "link");
    Path linkToDir      = new Path(testBaseDir2(), "linkToDir");
    Path linkViaLink    = new Path(linkToDir, "link");
    Path linkNewViaLink = new Path(linkToDir, "linkNew");
    createAndWriteFile(file);
    fc.createSymlink(file, link, false);
    fc.createSymlink(baseDir, linkToDir, false);
    fc.rename(linkViaLink, linkNewViaLink);
    assertFalse(exists(fc, linkViaLink));
    // Check that we didn't rename the link target
    assertTrue(exists(fc, file));
    assertTrue(fc.getFileLinkStatus(linkNewViaLink).isSymlink());
    readFile(linkNewViaLink);
  }

  @Test
  /** Test rename a directory to a symlink to a directory */
  public void testRenameDirToSymlinkToDir() throws IOException {
    Path dir1      = new Path(testBaseDir1());
    Path subDir = new Path(testBaseDir2(), "subDir");
    Path linkToDir = new Path(testBaseDir2(), "linkToDir");
    fc.mkdir(subDir, FileContext.DEFAULT_PERM, false);
    fc.createSymlink(subDir, linkToDir, false);
    try {
      fc.rename(dir1, linkToDir, Rename.OVERWRITE);
      fail("Renamed directory to a symlink");
    } catch (IOException e) {
      // Expected. Both must be directories.
      assertTrue(unwrapException(e) instanceof IOException);
    }
    assertTrue(exists(fc, dir1));
    assertTrue(exists(fc, linkToDir));
  }

  @Test
  /** Test rename a directory to a symlink to a file */
  public void testRenameDirToSymlinkToFile() throws IOException {
    Path dir1 = new Path(testBaseDir1());
    Path file = new Path(testBaseDir2(), "file");
    Path linkToFile = new Path(testBaseDir2(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(file, linkToFile, false);
    try {
      fc.rename(dir1, linkToFile, Rename.OVERWRITE);
      fail("Renamed directory to a symlink");
    } catch (IOException e) {
      // Expected. Both must be directories.
      assertTrue(unwrapException(e) instanceof IOException);
    }
    assertTrue(exists(fc, dir1));
    assertTrue(exists(fc, linkToFile));
  }

  @Test
  /** Test rename a directory to a dangling symlink */
  public void testRenameDirToDanglingSymlink() throws IOException {
    Path dir = new Path(testBaseDir1());
    Path link = new Path(testBaseDir2(), "linkToFile");
    fc.createSymlink(new Path("/doesNotExist"), link, false);
    try { 
      fc.rename(dir, link, Rename.OVERWRITE);
      fail("Renamed directory to a symlink"); 
    } catch (IOException e) {
      // Expected. Both must be directories.
      assertTrue(unwrapException(e) instanceof IOException);
    }
    assertTrue(exists(fc, dir));
    assertTrue(fc.getFileLinkStatus(link) != null);
  }

  @Test
  /** Test rename a file to a symlink to a directory */
  public void testRenameFileToSymlinkToDir() throws IOException {
    Path file   = new Path(testBaseDir1(), "file");
    Path subDir = new Path(testBaseDir1(), "subDir");
    Path link   = new Path(testBaseDir1(), "link");
    fc.mkdir(subDir, FileContext.DEFAULT_PERM, false);
    fc.createSymlink(subDir, link, false);
    createAndWriteFile(file);
    try {
      fc.rename(file, link);
      fail("Renamed file to symlink w/o overwrite"); 
    } catch (IOException e) {
      // Expected
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    fc.rename(file, link, Rename.OVERWRITE);
    assertFalse(exists(fc, file));
    assertTrue(exists(fc, link));
    assertTrue(isFile(fc, link));
    assertFalse(fc.getFileLinkStatus(link).isSymlink());
  }

  @Test
  /** Test rename a file to a symlink to a file */
  public void testRenameFileToSymlinkToFile() throws IOException {
    Path file1 = new Path(testBaseDir1(), "file1");
    Path file2 = new Path(testBaseDir1(), "file2");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file1);
    createAndWriteFile(file2);
    fc.createSymlink(file2, link, false);
    try {
      fc.rename(file1, link);
      fail("Renamed file to symlink w/o overwrite"); 
    } catch (IOException e) {
      // Expected
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    fc.rename(file1, link, Rename.OVERWRITE);
    assertFalse(exists(fc, file1));
    assertTrue(exists(fc, link));
    assertTrue(isFile(fc, link));
    assertFalse(fc.getFileLinkStatus(link).isSymlink());
  }

  @Test
  /** Test rename a file to a dangling symlink */
  public void testRenameFileToDanglingSymlink() throws IOException {
    /* NB: Local file system doesn't handle dangling links correctly
     * since File.exists(danglinLink) returns false. */ 
    if ("file".equals(getScheme())) {
      return;
    }
    Path file1 = new Path(testBaseDir1(), "file1");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file1);
    fc.createSymlink(new Path("/doesNotExist"), link, false);
    try {
      fc.rename(file1, link);  
    } catch (IOException e) {
      // Expected
    }
    fc.rename(file1, link, Rename.OVERWRITE);
    assertFalse(exists(fc, file1));
    assertTrue(exists(fc, link));
    assertTrue(isFile(fc, link));
    assertFalse(fc.getFileLinkStatus(link).isSymlink());    
  }

  @Test
  /** Rename a symlink to a new non-existant name */
  public void testRenameSymlinkNonExistantDest() throws IOException {
    Path file  = new Path(testBaseDir1(), "file");
    Path link1 = new Path(testBaseDir1(), "linkToFile1");
    Path link2 = new Path(testBaseDir1(), "linkToFile2");
    createAndWriteFile(file);
    fc.createSymlink(file, link1, false);
    fc.rename(link1, link2);
    assertTrue(fc.getFileLinkStatus(link2).isSymlink());
    readFile(link2);
    readFile(file);
    assertFalse(exists(fc, link1));
  }

  @Test
  /** Rename a symlink to a file that exists */
  public void testRenameSymlinkToExistingFile() throws IOException {
    Path file1 = new Path(testBaseDir1(), "file");
    Path file2 = new Path(testBaseDir1(), "someFile");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file1);
    createAndWriteFile(file2);
    fc.createSymlink(file2, link, false);
    try {
      fc.rename(link, file1);
      fail("Renamed w/o passing overwrite");
    } catch (IOException e) {
      // Expected
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    fc.rename(link, file1, Rename.OVERWRITE);
    assertFalse(exists(fc, link));
    assertTrue(fc.getFileLinkStatus(file1).isSymlink());
    assertEquals(file2, fc.getLinkTarget(file1));
  }

  @Test
  /** Rename a symlink to a directory that exists */
  public void testRenameSymlinkToExistingDir() throws IOException {
    Path dir1   = new Path(testBaseDir1());
    Path dir2   = new Path(testBaseDir2());
    Path subDir = new Path(testBaseDir2(), "subDir");
    Path link   = new Path(testBaseDir1(), "linkToDir");    
    fc.createSymlink(dir1, link, false);
    try {
      fc.rename(link, dir2);
      fail("Renamed link to a directory");
    } catch (IOException e) {
      // Expected. Both must be directories.
      assertTrue(unwrapException(e) instanceof IOException);
    }
    try {
      fc.rename(link, dir2, Rename.OVERWRITE);
      fail("Renamed link to a directory");
    } catch (IOException e) {
      // Expected. Both must be directories.
      assertTrue(unwrapException(e) instanceof IOException);
    }
    // Also fails when dir2 has a sub-directory
    fc.mkdir(subDir, FsPermission.getDefault(), false);
    try {
      fc.rename(link, dir2, Rename.OVERWRITE);
      fail("Renamed link to a directory");
    } catch (IOException e) {
      // Expected. Both must be directories.
      assertTrue(unwrapException(e) instanceof IOException);
    }
  }

  @Test
  /** Rename a symlink to itself */
  public void testRenameSymlinkToItself() throws IOException {
    Path link = new Path(testBaseDir1(), "linkToFile1");
    fc.createSymlink(new Path("/doestNotExist"), link, false);
    try {
      fc.rename(link, link);
    } catch (IOException e) {
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    // Fails with overwrite as well
    try {
      fc.rename(link, link, Rename.OVERWRITE);
    } catch (IOException e) {
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
  }

  @Test
  /** Rename a symlink */
  public void testRenameSymlink() throws IOException {
    Path file  = new Path(testBaseDir1(), "file");
    Path link1 = new Path(testBaseDir1(), "linkToFile1");
    Path link2 = new Path(testBaseDir1(), "linkToFile2");    
    createAndWriteFile(file);
    fc.createSymlink(file, link1, false);
    fc.rename(link1, link2);
    assertTrue(fc.getFileLinkStatus(link2).isSymlink());
    assertFalse(fc.getFileStatus(link2).isDirectory());
    readFile(link2);
    readFile(file);
    try {
      createAndWriteFile(link2);
      fail("link was not renamed");
    } catch (IOException x) {
      // Expected
    } 
  }

  @Test
  /** Rename a symlink to the file it links to */
  public void testRenameSymlinkToFileItLinksTo() throws IOException {
    /* NB: The rename is not atomic, so file is deleted before renaming
     * linkToFile. In this interval linkToFile is dangling and local file 
     * system does not handle dangling links because File.exists returns 
     * false for dangling links. */
    if ("file".equals(getScheme())) {
      return;
    }
    Path file = new Path(testBaseDir1(), "file");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(file, link, false);
    try {
      fc.rename(link, file);
      fail("Renamed symlink to its target");
    } catch (IOException e) {
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    // Check the rename didn't happen
    assertTrue(isFile(fc, file));
    assertTrue(exists(fc, link));
    assertTrue(isSymlink(fc, link));
    assertEquals(file, fc.getLinkTarget(link));
    try {
      fc.rename(link, file, Rename.OVERWRITE);
      fail("Renamed symlink to its target");
    } catch (IOException e) {
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    // Check the rename didn't happen
    assertTrue(isFile(fc, file));
    assertTrue(exists(fc, link));    
    assertTrue(isSymlink(fc, link));
    assertEquals(file, fc.getLinkTarget(link));    
  }
  
  @Test
  /** Rename a symlink to the directory it links to */
  public void testRenameSymlinkToDirItLinksTo() throws IOException {
    /* NB: The rename is not atomic, so dir is deleted before renaming
     * linkToFile. In this interval linkToFile is dangling and local file 
     * system does not handle dangling links because File.exists returns 
     * false for dangling links. */
    if ("file".equals(getScheme())) {
      return;
    }
    Path dir  = new Path(testBaseDir1(), "dir");
    Path link = new Path(testBaseDir1(), "linkToDir");
    fc.mkdir(dir, FileContext.DEFAULT_PERM, false);
    fc.createSymlink(dir, link, false);
    try {
      fc.rename(link, dir);
      fail("Renamed symlink to its target");
    } catch (IOException e) {
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    // Check the rename didn't happen
    assertTrue(isDir(fc, dir));
    assertTrue(exists(fc, link));
    assertTrue(isSymlink(fc, link));
    assertEquals(dir, fc.getLinkTarget(link));
    try {
      fc.rename(link, dir, Rename.OVERWRITE);
      fail("Renamed symlink to its target");
    } catch (IOException e) {
      assertTrue(unwrapException(e) instanceof FileAlreadyExistsException);
    }
    // Check the rename didn't happen
    assertTrue(isDir(fc, dir));
    assertTrue(exists(fc, link));
    assertTrue(isSymlink(fc, link));
    assertEquals(dir, fc.getLinkTarget(link));
  }
  
  @Test
  /** Test rename the symlink's target */
  public void testRenameLinkTarget() throws IOException {
    Path file    = new Path(testBaseDir1(), "file");
    Path fileNew = new Path(testBaseDir1(), "fileNew");
    Path link    = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(file, link, false);
    fc.rename(file, fileNew, Rename.OVERWRITE);
    try {
      readFile(link);        
      fail("Link should be dangling");
    } catch (IOException x) {
      // Expected
    }
    fc.rename(fileNew, file, Rename.OVERWRITE);
    readFile(link);
  }

  @Test
  /** Test rename a file to path with destination that has symlink parent */
  public void testRenameFileWithDestParentSymlink() throws IOException {
    Path link  = new Path(testBaseDir1(), "link");
    Path file1 = new Path(testBaseDir1(), "file1");    
    Path file2 = new Path(testBaseDir1(), "file2");    
    Path file3 = new Path(link, "file3");
    Path dir2  = new Path(testBaseDir2());
    
    // Renaming /dir1/file1 to non-existant file /dir1/link/file3 is OK
    // if link points to a directory...
    fc.createSymlink(dir2, link, false);
    createAndWriteFile(file1);
    fc.rename(file1, file3);
    assertFalse(exists(fc, file1));
    assertTrue(exists(fc, file3));
    fc.rename(file3, file1);
    
    // But fails if link is dangling...
    fc.delete(link, false);
    fc.createSymlink(file2, link, false);    
    try {
      fc.rename(file1, file3);
    } catch (IOException e) {
      // Expected
      assertTrue(unwrapException(e) instanceof FileNotFoundException);
    }

    // And if link points to a file...
    createAndWriteFile(file2);
    try {
      fc.rename(file1, file3);
    } catch (IOException e) {
      // Expected
      assertTrue(unwrapException(e) instanceof ParentNotDirectoryException);
    }    
  }
  
  @Test
  /**
   * Create, write, read, append, rename, get the block locations,
   * checksums, and delete a file using a path with a symlink as an
   * intermediate path component where the link target was specified
   * using an absolute path. Rename is covered in more depth below.
   */
  public void testAccessFileViaInterSymlinkAbsTarget() throws IOException {
    Path baseDir        = new Path(testBaseDir1());
    Path file           = new Path(testBaseDir1(), "file");
    Path fileNew        = new Path(baseDir, "fileNew");
    Path linkToDir      = new Path(testBaseDir2(), "linkToDir");
    Path fileViaLink    = new Path(linkToDir, "file");
    Path fileNewViaLink = new Path(linkToDir, "fileNew");
    fc.createSymlink(baseDir, linkToDir, false);
    createAndWriteFile(fileViaLink);
    assertTrue(exists(fc, fileViaLink));
    assertTrue(isFile(fc, fileViaLink));
    assertFalse(isDir(fc, fileViaLink));
    assertFalse(fc.getFileLinkStatus(fileViaLink).isSymlink());
    assertFalse(isDir(fc, fileViaLink));
    assertEquals(fc.getFileStatus(file),
                 fc.getFileLinkStatus(file));
    assertEquals(fc.getFileStatus(fileViaLink),
                 fc.getFileLinkStatus(fileViaLink));
    readFile(fileViaLink);
    appendToFile(fileViaLink);
    fc.rename(fileViaLink, fileNewViaLink);
    assertFalse(exists(fc, fileViaLink));
    assertTrue(exists(fc, fileNewViaLink));
    readFile(fileNewViaLink);
    assertEquals(fc.getFileBlockLocations(fileNew, 0, 1).length,
                 fc.getFileBlockLocations(fileNewViaLink, 0, 1).length);
    assertEquals(fc.getFileChecksum(fileNew),
                 fc.getFileChecksum(fileNewViaLink));
    fc.delete(fileNewViaLink, true);
    assertFalse(exists(fc, fileNewViaLink));
  }

  @Test
  /**
   * Operate on a file using a path with an intermediate symlink where
   * the link target was specified as a fully qualified path.
   */
  public void testAccessFileViaInterSymlinkQualTarget() throws IOException {
    Path baseDir        = new Path(testBaseDir1());
    Path file           = new Path(testBaseDir1(), "file");
    Path fileNew        = new Path(baseDir, "fileNew");
    Path linkToDir      = new Path(testBaseDir2(), "linkToDir");
    Path fileViaLink    = new Path(linkToDir, "file");
    Path fileNewViaLink = new Path(linkToDir, "fileNew");
    fc.createSymlink(fc.makeQualified(baseDir), linkToDir, false);
    createAndWriteFile(fileViaLink);
    assertEquals(fc.getFileStatus(file),
                 fc.getFileLinkStatus(file));
    assertEquals(fc.getFileStatus(fileViaLink),
                 fc.getFileLinkStatus(fileViaLink));
    readFile(fileViaLink);
  }

  @Test
  /**
   * Operate on a file using a path with an intermediate symlink where
   * the link target was specified as a relative path.
   */
  public void testAccessFileViaInterSymlinkRelTarget() throws IOException {
    assumeTrue(!"file".equals(getScheme()));
    Path baseDir     = new Path(testBaseDir1());
    Path dir         = new Path(testBaseDir1(), "dir");
    Path file        = new Path(dir, "file");
    Path linkToDir   = new Path(testBaseDir1(), "linkToDir");
    Path fileViaLink = new Path(linkToDir, "file");

    fc.mkdir(dir, FileContext.DEFAULT_PERM, false);
    fc.createSymlink(new Path("dir"), linkToDir, false);
    createAndWriteFile(fileViaLink);
    // Note that getFileStatus returns fully qualified paths even
    // when called on an absolute path.
    assertEquals(fc.makeQualified(file),
                 fc.getFileStatus(file).getPath());
    // In each case getFileLinkStatus returns the same FileStatus
    // as getFileStatus since we're not calling it on a link and
    // FileStatus objects are compared by Path.
    assertEquals(fc.getFileStatus(file),
                 fc.getFileLinkStatus(file));
    assertEquals(fc.getFileStatus(fileViaLink),
                 fc.getFileLinkStatus(fileViaLink));
    assertEquals(fc.getFileStatus(fileViaLink),
                 fc.getFileLinkStatus(file));
  }

  @Test
  /** Test create, list, and delete a directory through a symlink */
  public void testAccessDirViaSymlink() throws IOException {
    Path baseDir    = new Path(testBaseDir1());
    Path dir        = new Path(testBaseDir1(), "dir");
    Path linkToDir  = new Path(testBaseDir2(), "linkToDir");
    Path dirViaLink = new Path(linkToDir, "dir");
    fc.createSymlink(baseDir, linkToDir, false);
    fc.mkdir(dirViaLink, FileContext.DEFAULT_PERM, true);
    assertTrue(fc.getFileStatus(dirViaLink).isDirectory());
    FileStatus[] stats = fc.util().listStatus(dirViaLink);
    assertEquals(0, stats.length);
    RemoteIterator<FileStatus> statsItor = fc.listStatus(dirViaLink);
    assertFalse(statsItor.hasNext());
    fc.delete(dirViaLink, false);
    assertFalse(exists(fc, dirViaLink));
    assertFalse(exists(fc, dir));
  }

  @Test
  /** setTimes affects the target not the link */    
  public void testSetTimes() throws IOException {
    Path file = new Path(testBaseDir1(), "file");
    Path link = new Path(testBaseDir1(), "linkToFile");
    createAndWriteFile(file);
    fc.createSymlink(file, link, false);
    long at = fc.getFileLinkStatus(link).getAccessTime(); 
    fc.setTimes(link, 2L, 3L);
    // NB: local file systems don't implement setTimes
    if (!"file".equals(getScheme())) {
      assertEquals(at, fc.getFileLinkStatus(link).getAccessTime());
      assertEquals(3, fc.getFileStatus(file).getAccessTime());
      assertEquals(2, fc.getFileStatus(file).getModificationTime());
    }
  }
}
