tikhomirov@64: /*
tikhomirov@431:  * Copyright (c) 2011-2012 TMate Software Ltd
tikhomirov@64:  *  
tikhomirov@64:  * This program is free software; you can redistribute it and/or modify
tikhomirov@64:  * it under the terms of the GNU General Public License as published by
tikhomirov@64:  * the Free Software Foundation; version 2 of the License.
tikhomirov@64:  *
tikhomirov@64:  * This program is distributed in the hope that it will be useful,
tikhomirov@64:  * but WITHOUT ANY WARRANTY; without even the implied warranty of
tikhomirov@64:  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
tikhomirov@64:  * GNU General Public License for more details.
tikhomirov@64:  *
tikhomirov@64:  * For information on how to redistribute this software under
tikhomirov@64:  * the terms of a license other than GNU General Public License
tikhomirov@102:  * contact TMate Software at support@hg4j.com
tikhomirov@64:  */
tikhomirov@133: package org.tmatesoft.hg.util;
tikhomirov@64: 
tikhomirov@229: import java.util.Collection;
tikhomirov@339: import java.util.Iterator;
tikhomirov@339: import java.util.NoSuchElementException;
tikhomirov@229: 
tikhomirov@64: /**
tikhomirov@64:  * Identify repository files (not String nor io.File). Convenient for pattern matching. Memory-friendly.
tikhomirov@64:  * 
tikhomirov@64:  * @author Artem Tikhomirov
tikhomirov@64:  * @author TMate Software Ltd.
tikhomirov@64:  */
tikhomirov@64: public final class Path implements CharSequence, Comparable<Path>/*Cloneable? - although clone for paths make no sense*/{
tikhomirov@64: //	private String[] segments;
tikhomirov@64: //	private int flags; // dir, unparsed
tikhomirov@64: 	private String path;
tikhomirov@64: 	
tikhomirov@64: 	/*package-local*/Path(String p) {
tikhomirov@64: 		path = p;
tikhomirov@64: 	}
tikhomirov@64: 
tikhomirov@148: 	/**
tikhomirov@148: 	 * Check if this is directory's path. 
tikhomirov@148: 	 * Note, this method doesn't perform any file system operation.
tikhomirov@148: 	 * 
tikhomirov@148: 	 * @return true when this path points to a directory 
tikhomirov@148: 	 */
tikhomirov@148: 	public boolean isDirectory() {
tikhomirov@148: 		// XXX simple logic for now. Later we may decide to have an explicit factory method to create directory paths
tikhomirov@148: 		return path.charAt(path.length() - 1) == '/';
tikhomirov@148: 	}
tikhomirov@148: 
tikhomirov@64: 	public int length() {
tikhomirov@64: 		return path.length();
tikhomirov@64: 	}
tikhomirov@64: 
tikhomirov@64: 	public char charAt(int index) {
tikhomirov@64: 		return path.charAt(index);
tikhomirov@64: 	}
tikhomirov@64: 
tikhomirov@64: 	public CharSequence subSequence(int start, int end) {
tikhomirov@64: 		// new Path if start-end matches boundaries of any subpath
tikhomirov@64: 		return path.substring(start, end);
tikhomirov@64: 	}
tikhomirov@64: 	
tikhomirov@64: 	@Override
tikhomirov@64: 	public String toString() {
tikhomirov@64: 		return path; // CharSequence demands toString() impl
tikhomirov@64: 	}
tikhomirov@339: 	
tikhomirov@339: 	public Iterable<String> segments() {
tikhomirov@339: 		class SegSeq implements Iterable<String>, Iterator<String> {
tikhomirov@339: 			private int pos; // first char to return
tikhomirov@339: 
tikhomirov@339: 			public Iterator<String> iterator() {
tikhomirov@339: 				reset();
tikhomirov@339: 				return this;
tikhomirov@339: 			}
tikhomirov@339: 			public boolean hasNext() {
tikhomirov@339: 				return pos < path.length();
tikhomirov@339: 			}
tikhomirov@339: 			public String next() {
tikhomirov@339: 				if (pos >= path.length()) {
tikhomirov@339: 					throw new NoSuchElementException();
tikhomirov@339: 				}
tikhomirov@339: 				int x = path.indexOf('/', pos);
tikhomirov@339: 				if (x == -1) {
tikhomirov@339: 					String rv = path.substring(pos);
tikhomirov@339: 					pos = path.length();
tikhomirov@339: 					return rv;
tikhomirov@339: 				} else {
tikhomirov@339: 					String rv = path.substring(pos, x);
tikhomirov@339: 					pos = x+1;
tikhomirov@339: 					return rv;
tikhomirov@339: 				}
tikhomirov@339: 			}
tikhomirov@339: 			public void remove() {
tikhomirov@339: 				throw new UnsupportedOperationException();
tikhomirov@339: 			}
tikhomirov@339: 
tikhomirov@339: 			private void reset() {
tikhomirov@339: 				pos = 0;
tikhomirov@339: 			}
tikhomirov@339: 		};
tikhomirov@339: 		return new SegSeq();
tikhomirov@339: 	}
tikhomirov@64: 
tikhomirov@64: 	public int compareTo(Path o) {
tikhomirov@64: 		return path.compareTo(o.path);
tikhomirov@64: 	}
tikhomirov@64: 	
tikhomirov@64: 	@Override
tikhomirov@64: 	public boolean equals(Object obj) {
tikhomirov@64: 		if (obj != null && getClass() == obj.getClass()) {
tikhomirov@64: 			return this == obj || path.equals(((Path) obj).path);
tikhomirov@64: 		}
tikhomirov@64: 		return false;
tikhomirov@64: 	}
tikhomirov@64: 	@Override
tikhomirov@64: 	public int hashCode() {
tikhomirov@64: 		return path.hashCode();
tikhomirov@64: 	}
tikhomirov@229: 	
tikhomirov@229: 	public enum CompareResult {
tikhomirov@443: 		Same, Unrelated, ImmediateChild, Nested, ImmediateParent, Parent /* +CommonParent ?*/
tikhomirov@229: 	}
tikhomirov@229: 	
tikhomirov@443: 	/**
tikhomirov@443: 	 * @return one of {@link CompareResult} constants to indicate relations between the paths 
tikhomirov@229: 	 */
tikhomirov@229: 	public CompareResult compareWith(Path another) {
tikhomirov@229: 		if (another == null) {
tikhomirov@229: 			return CompareResult.Unrelated; // XXX perhaps, IAE?
tikhomirov@229: 		}
tikhomirov@229: 		if (another == this || (another.length() == length() && equals(another))) {
tikhomirov@229: 			return CompareResult.Same;
tikhomirov@229: 		}
tikhomirov@443: 		// one of the parties can't be parent in parent/nested, the other may be either file or folder 
tikhomirov@443: 		if (another.isDirectory() && path.startsWith(another.path)) {
tikhomirov@443: 			return isOneSegmentDifference(path, another.path) ? CompareResult.ImmediateChild : CompareResult.Nested;
tikhomirov@229: 		}
tikhomirov@443: 		if (isDirectory() && another.path.startsWith(path)) {
tikhomirov@443: 			return isOneSegmentDifference(another.path, path) ? CompareResult.ImmediateParent : CompareResult.Parent;
tikhomirov@229: 		}
tikhomirov@229: 		return CompareResult.Unrelated;
tikhomirov@229: 	}
tikhomirov@443: 	
tikhomirov@443: 	// true if p1 is only one segment larger than p2
tikhomirov@443: 	private static boolean isOneSegmentDifference(String p1, String p2) {
tikhomirov@443: 		assert p1.startsWith(p2);
tikhomirov@443: 		String p1Tail= p1.substring(p2.length());
tikhomirov@443: 		int slashLoc = p1Tail.indexOf('/');
tikhomirov@443: 		return slashLoc == -1 || slashLoc == p1Tail.length() - 1;
tikhomirov@443: 	}
tikhomirov@64: 
tikhomirov@292: 	public static Path create(CharSequence path) {
tikhomirov@64: 		if (path == null) {
tikhomirov@64: 			throw new IllegalArgumentException();
tikhomirov@64: 		}
tikhomirov@292: 		if (path instanceof Path) {
tikhomirov@292: 			Path o = (Path) path;
tikhomirov@292: 			return o;
tikhomirov@292: 		}
tikhomirov@292: 		String p = path.toString();
tikhomirov@292: 		if (p.indexOf('\\') != -1) {
tikhomirov@77: 			throw new IllegalArgumentException();
tikhomirov@77: 		}
tikhomirov@292: 		Path rv = new Path(p);
tikhomirov@64: 		return rv;
tikhomirov@64: 	}
tikhomirov@133: 
tikhomirov@133: 	/**
tikhomirov@133: 	 * Path filter.
tikhomirov@133: 	 */
tikhomirov@64: 	public interface Matcher {
tikhomirov@141: 		boolean accept(Path path);
tikhomirov@229: 		
tikhomirov@229: 		final class Any implements Matcher {
tikhomirov@229: 			public boolean accept(Path path) { return true; }
tikhomirov@229: 		}
tikhomirov@229: 		class Composite implements Matcher {
tikhomirov@229: 			private final Path.Matcher[] elements;
tikhomirov@229: 			
tikhomirov@229: 			public Composite(Collection<Path.Matcher> matchers) {
tikhomirov@229: 				elements = matchers.toArray(new Path.Matcher[matchers.size()]);
tikhomirov@229: 			}
tikhomirov@229: 
tikhomirov@229: 			public boolean accept(Path path) {
tikhomirov@229: 				for (Path.Matcher m : elements) {
tikhomirov@229: 					if (m.accept(path)) {
tikhomirov@229: 						return true;
tikhomirov@229: 					}
tikhomirov@229: 				}
tikhomirov@229: 				return false;
tikhomirov@229: 			}
tikhomirov@229: 		}
tikhomirov@141: 	}
tikhomirov@141: 
tikhomirov@141: 	/**
tikhomirov@141: 	 * Factory for paths
tikhomirov@141: 	 */
tikhomirov@141: 	public interface Source {
tikhomirov@431: 		Path path(CharSequence p);
tikhomirov@141: 	}
tikhomirov@431: 	
tikhomirov@141: 	/**
tikhomirov@141: 	 * Straightforward {@link Source} implementation that creates new Path instance for each supplied string
tikhomirov@431: 	 * and optionally piping through a converter to get e.g. cached instance 
tikhomirov@141: 	 */
tikhomirov@141: 	public static class SimpleSource implements Source {
tikhomirov@141: 		private final PathRewrite normalizer;
tikhomirov@431: 		private final Convertor<Path> convertor;
tikhomirov@431: 
tikhomirov@431: 		public SimpleSource() {
tikhomirov@431: 			this(new PathRewrite.Empty(), null);
tikhomirov@431: 		}
tikhomirov@141: 
tikhomirov@141: 		public SimpleSource(PathRewrite pathRewrite) {
tikhomirov@431: 			this(pathRewrite, null);
tikhomirov@141: 		}
tikhomirov@141: 
tikhomirov@431: 		public SimpleSource(PathRewrite pathRewrite, Convertor<Path> pathConvertor) {
tikhomirov@431: 			normalizer = pathRewrite;
tikhomirov@431: 			convertor = pathConvertor;
tikhomirov@431: 		}
tikhomirov@431: 
tikhomirov@431: 		public Path path(CharSequence p) {
tikhomirov@431: 			Path rv = Path.create(normalizer.rewrite(p));
tikhomirov@431: 			if (convertor != null) {
tikhomirov@431: 				return convertor.mangle(rv);
tikhomirov@431: 			}
tikhomirov@431: 			return rv;
tikhomirov@141: 		}
tikhomirov@64: 	}
tikhomirov@64: }