/*
* Copyright (C) 2012-2014 University of Dundee & Open Microscopy Environment.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package ome.services.blitz.repo.path;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
/**
* An analog of {@link File} representing an FS repository file-path.
* The file-path is relative to the root of the repository. As with
* {@link File}, instances of this class are immutable.
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.0
*/
public class FsFile {
/** the separator character for delimiting repository path components */
public static char separatorChar = '/';
/** the FsFile that has no path components */
public static FsFile emptyPath = new FsFile();
/* the components of this path */
private final List<String> components;
/* the string representation of this path
* (i.e. its components with separatorChar in between each pair) */
private final String path;
/**
* Split path components by the given separator.
* Adjacent separators are considered to be only one.
* @param path the path to split
* @param separator the separator by which to split
* @return the path components
*/
private static List<String> splitComponents(String path, char separator) {
final String[] splitBySeparator = path.split("\\" + separator);
final List<String> components = new ArrayList<String>(splitBySeparator.length);
for (final String component : splitBySeparator) {
if (!"".equals(component))
components.add(component);
}
return components;
}
/**
* Construct an instance.
* @param components the components of the path to which this instance corresponds, may be null
*/
public FsFile(Collection<String> components) {
if (components == null || components.isEmpty()) {
this.components = Collections.emptyList();
this.path = "";
} else {
this.components = Collections.unmodifiableList(new ArrayList<String>(components));
final StringBuilder pathBuilder = new StringBuilder();
for (final String component : components) {
if (StringUtils.isEmpty(component))
throw new IllegalArgumentException("each path component must have content");
if (component.indexOf(separatorChar) != -1)
throw new IllegalArgumentException("path components may not contain a path separator");
pathBuilder.append(component);
pathBuilder.append(FsFile.separatorChar);
}
pathBuilder.setLength(pathBuilder.length() - 1);
this.path = pathBuilder.toString();
}
}
/**
* Construct an instance.
* @param components the components of the path to which this instance corresponds
*/
public FsFile(String... components) {
this(Arrays.asList(components));
}
/**
* Return a view of the last <em>n</em> elements of a list. If the list is
* shorter than <em>n</em>, returns a view of the whole list without padding it.
* @param list a list
* @param count how many (<em>n</em>) elements to view, must be positive
* @return a view of the list's last <em>n</em> elements
*/
private static <X> List<X> tailOf(List<X> list, int count) {
if (count < 0)
throw new IllegalArgumentException("count must be positive");
final int listSize = list.size();
final int startIndex = listSize > count ? listSize - count : 0;
return list.subList(startIndex, listSize);
}
/**
* Construct an instance by trimming parent directories from an existing instance
* such that the depth does not exceed the given maximum.
* Allows ignoring of client-side parent directories beyond those specified by the user.
* @param file an existing instance
* @param maxComponentCount the number of child components of the instance,
* including filename, above which parents should be ignored
*/
public FsFile(FsFile file, int maxComponentCount) {
this(FsFile.tailOf(file.components, maxComponentCount));
}
/**
* Construct an instance.
* @param file the file to whose absolute path this instance corresponds
*/
public FsFile(File file) {
this(splitComponents(file.getAbsolutePath(), File.separatorChar));
}
/**
* Construct an instance.
* @param path the path that this instance's string representation must match
*/
public FsFile(String path) {
this(splitComponents(path, FsFile.separatorChar));
}
/**
* Transform each path component with the given transformer.
* @param componentTransformer a transformer
* @return the transformed path
*/
public FsFile transform(Function<String, String> componentTransformer) {
return new FsFile(Lists.transform(this.components, componentTransformer));
}
/**
* Find the relative path of this path from a given parent.
* Allows adjustment of absolute paths to the repository's root directory.
* Matches path component names case-sensitively.
* @param file a parent path (may be the same as this one)
* @return the relative path, or null if none exists
*/
public FsFile getPathFrom(FsFile file) {
final Iterator<String> container = file.components.iterator();
final Iterator<String> contained = this.components.iterator();
for (;;) {
if (!container.hasNext()) {
/* now descended into parent */
final List<String> childComponents = new ArrayList<String>();
while (contained.hasNext())
childComponents.add(contained.next());
return new FsFile(childComponents);
} else if (!contained.hasNext())
/* the "parent" is actually a child of this instance */
return null;
else if (!container.next().equals(contained.next())) {
/* neither this instance nor the "parent" contain the other */
return null;
}
}
}
/**
* Concatenate paths.
* @param files the paths to concatenate
* @return the concatenated path
*/
public static FsFile concatenate(FsFile... files) {
int size = 0;
for (final FsFile file : files)
size += file.components.size();
final List<String> components = new ArrayList<String>(size);
for (final FsFile file : files)
components.addAll(file.components);
return new FsFile(components);
}
/**
* Get the path components of this instance.
* @return the path components, never null
*/
public List<String> getComponents() {
return this.components;
}
/**
* Convert this instance to a {@link File}
* relative to the given {@link File}.
* @param file parent directory, may be null for a relative return value,
* but actually expected to be the repository's root directory
* @return where this instance should be located in the server-side filesystem
*/
public File toFile(File file) {
for (final String component : this.components)
file = new File(file, component);
return file;
}
/**
* {@inheritDoc}
* Provides repository path with components separated by {@link #separatorChar}.
* Suitable for displaying to the user and for constructing a new instance.
*/
@Override
public String toString() {
return this.path;
}
/**
* {@inheritDoc}
* Instances are equal if their string representations match.
*/
@Override
public boolean equals(Object object) {
if (this == object)
return true;
if (!(object instanceof FsFile))
return false;
return this.path.equals(((FsFile) object).path);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return this.path.hashCode() * 97;
}
}