LookupAwareRelativePathResolver.java

package fr.sii.ogham.core.resource.path;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import fr.sii.ogham.core.util.ResourceUtils;

/**
 * Resolve the given path against the base path by using the following rules:
 * 
 * <p>
 * If the <b>relative path</b> is not absolute (doesn't start with /) and the
 * <b>base path</b> ends with / then the <b>relative path</b> is <b>appended</b>
 * to the <b>base path</b>.
 * </p>
 * <p>
 * If the <b>relative path</b> is not absolute (doesn't start with /) and the
 * <b>base path</b> doesn't end with / then the <b>relative path</b>
 * <b>replaces</b> the last part of the <b>base path</b>.
 * </p>
 * <p>
 * If the <b>relative path</b> is absolute (starts with /) then the <b>relative
 * path</b> is is used.
 * </p>
 * <p>
 * If the <b>relative path</b> has lookup prefix (like "classpath:") then this
 * lookup prefix is used.
 * </p>
 * <p>
 * If the <b>relative path</b> has no lookup prefix then the lookup prefix of
 * <b>base path</b> is used (if any).
 * </p>
 * <p>
 * If the <b>relative path</b> has different lookup prefix than <b>base path</b>
 * lookup then relative path is considered like an absolute path.
 * </p>
 * 
 * <table>
 * <caption>Exemple of results</caption> <thead>
 * <tr>
 * <th>description</th>
 * <th>base path</th>
 * <th>relative path</th>
 * <th>result</th>
 * </tr>
 * </thead> <tbody>
 * <tr>
 * <td>resolve as child</td>
 * <td>classpath:/template/</td>
 * <td>images/foo.png</td>
 * <td>classpath:/template/images/foo.png</td>
 * </tr>
 * <tr>
 * <td>resolve as sibling</td>
 * <td>classpath:/template/register.html</td>
 * <td>images/foo.png</td>
 * <td>classpath:/template/images/foo.png</td>
 * </tr>
 * <tr>
 * <td>resolve as sibling</td>
 * <td>classpath:/template</td>
 * <td>images/foo.png</td>
 * <td>classpath:/images/foo.png</td>
 * </tr>
 * <tr>
 * <td>absolute path</td>
 * <td>/template/</td>
 * <td>/images/foo.png</td>
 * <td>/images/foo.png</td>
 * </tr>
 * <tr>
 * <td>absolute path (keep lookup)</td>
 * <td>classpath:/template/</td>
 * <td>/images/foo.png</td>
 * <td>classpath:/images/foo.png</td>
 * </tr>
 * <tr>
 * <td>different lookups</td>
 * <td>classpath:/template/</td>
 * <td>file:images/foo.png</td>
 * <td>file:images/foo.png</td>
 * </tr>
 * </tbody>
 * </table>
 * 
 * <p>
 * This implementation that requires the list of known lookups.
 * </p>
 * 
 * @author Aurélien Baudet
 *
 */
public class LookupAwareRelativePathResolver implements RelativePathResolver {
	private final List<LookupMetadata> lookupsMeta;

	/**
	 * Initializes with the list of known lookups (indexed by type).
	 * 
	 * @param lookupsIndexedByType
	 *            the lookups indexed by type
	 */
	public LookupAwareRelativePathResolver(Map<String, List<String>> lookupsIndexedByType) {
		super();
		this.lookupsMeta = new ArrayList<>();
		for (Entry<String, List<String>> entry : lookupsIndexedByType.entrySet()) {
			for (String lookup : entry.getValue()) {
				if (!lookup.isEmpty()) {
					lookupsMeta.add(new LookupMetadata(lookup, entry.getKey()));
				}
			}
		}
	}

	@Override
	public RelativePath resolve(ResourcePath source, ResourcePath relativePath) {
		return new RelativePath(source, relativePath, getMergedPath(source, relativePath));
	}

	@Override
	public RelativePath resolve(ResourcePath source, String relativePath) {
		return resolve(source, new UnresolvedPath(relativePath));
	}

	private String getMergedPath(ResourcePath source, ResourcePath relativePath) {
		String relativeLookup = getLookup(relativePath.getOriginalPath());
		String sourceLookup = getLookup(source.getOriginalPath());
		// not the same lookup
		// => not the same system to load
		// => not relative
		if (relativeLookup != null && !isSameLookupType(relativeLookup, sourceLookup)) {
			return relativePath.getOriginalPath();
		}
		// if path points to a directory
		// => append the relative path
		// if path points to a file
		// => replace file by the relative path
		Path merged = Paths.get(withoutLookup(source instanceof ResolvedPath ? ((ResolvedPath) source).getResolvedPath() : source.getOriginalPath()));
		if (isDirectory(source.getOriginalPath())) {
			merged = merged.resolve(withoutLookup(relativePath.getOriginalPath()));
		} else {
			merged = merged.resolveSibling(withoutLookup(relativePath.getOriginalPath()));
		}
		if (relativeLookup != null) {
			return relativeLookup + ResourceUtils.toResourcePath(merged);
		}
		if (sourceLookup != null) {
			return sourceLookup + ResourceUtils.toResourcePath(merged);
		}
		return ResourceUtils.toResourcePath(merged);
	}

	private boolean isSameLookupType(String relativeLookup, String sourceLookup) {
		LookupMetadata relativeMeta = getLookupMeta(relativeLookup);
		if (relativeMeta == null) {
			return false;
		}
		return relativeMeta.isSameType(getLookupMeta(sourceLookup));
	}

	private LookupMetadata getLookupMeta(String lookup) {
		for (LookupMetadata meta : lookupsMeta) {
			if (meta.getLookup().equals(lookup)) {
				return meta;
			}
		}
		return null;
	}

	private static boolean isDirectory(String path) {
		return path.endsWith("/");
	}

	private String withoutLookup(String path) {
		for (LookupMetadata meta : lookupsMeta) {
			if (!meta.getLookup().isEmpty() && path.startsWith(meta.getLookup())) {
				return path.substring(meta.getLookup().length());
			}
		}
		return path;
	}

	private String getLookup(String path) {
		for (LookupMetadata meta : lookupsMeta) {
			if (!meta.getLookup().isEmpty() && path.startsWith(meta.getLookup())) {
				return meta.getLookup();
			}
		}
		return null;
	}

	private static class LookupMetadata {
		private final String lookup;
		private final String type;

		public LookupMetadata(String lookup, String type) {
			super();
			this.lookup = lookup;
			this.type = type;
		}

		public boolean isSameType(LookupMetadata lookupMeta) {
			if (lookupMeta == null) {
				return false;
			}
			return type.equals(lookupMeta.getType());
		}

		public String getLookup() {
			return lookup;
		}

		public String getType() {
			return type;
		}
	}
}