All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.daisy.streamline.api.media.DefaultFileSet Maven / Gradle / Ivy

The newest version!
package org.daisy.streamline.api.media;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Provides a default file set.
 * @author Joel Håkansson
 */
public final class DefaultFileSet implements ModifiableFileSet {
	private static final Logger logger = Logger.getLogger(DefaultFileSet.class.getCanonicalName());
	private final BaseFolder baseFolder;
	private final String manifestPath;
	private final Optional formatIdentifier;
	private final Map resources;
	
	/**
	 * Provides a builder of file sets.
	 */
	public static class Builder {
		private final BaseFolder baseFolder;
		private final String manifestPath;
		private FormatIdentifier formatIdentifier  = null;
		private final Map resources = new HashMap<>();

		/**
		 * Creates a new builder with the specified base folder and manifest.
		 * @param baseFolder the base folder
		 * @param manifest the manifest
		 * @throws IllegalArgumentException if the manifest isn't a descendant of the base folder
		 */
		public Builder(BaseFolder baseFolder, AnnotatedFile manifest) {
			this(baseFolder, manifest, baseFolder.getPath().relativize(requireDescendant(baseFolder.getPath(), manifest.getPath())).toString()); 
		}

		/**
		 * Creates a new builder with the specified base folder, manifest and manifest path.
		 * @param baseFolder the base folder
		 * @param manifest the manifest file
		 * @param manifestPath the path to the manifest within the file set
		 */
		public Builder(BaseFolder baseFolder, AnnotatedFile manifest, String manifestPath) {
			this.baseFolder = baseFolder;
			this.manifestPath = manifestPath;
			add(manifest, manifestPath);
		}
		
		/**
		 * Creates a new builder with the specified resources.
		 * @param manifest the manifest
		 * @param resources the resources
		 * @throws IllegalArgumentException if the resources do not share the same file root
		 */
		public Builder(AnnotatedFile manifest, AnnotatedFile ... resources) {
			this(manifest, Arrays.asList(resources));
		}

		/**
		 * Creates a new builder with the specified resources.
		 * @param manifest the manifest
		 * @param resources the resources
		 * @throws IllegalArgumentException if the resources do not share the same file root
		 */
		public Builder(AnnotatedFile manifest, Collection resources) {
			this(BaseFolder.with(findCommonAncestor(
					Stream.concat(resources.stream().map(v->v.getPath()), Stream.of(manifest.getPath()))
				).orElseThrow(IllegalArgumentException::new)), manifest);
			resources.forEach(v->add(v));
		}

		/**
		 * Sets the format identifier for this builder.
		 * @param value the format identifier
		 * @return this builder
		 */
		public Builder formatIdentifier(FormatIdentifier value) {
			this.formatIdentifier = value;
			return this;
		}

		/**
		 * Adds a resource to this builder.
		 * @param f the file
		 * @return this builder
		 */
		public Builder add(Path f) {
			add(DefaultAnnotatedFile.with(f).build());
			return this;
		}

		/**
		 * Adds a resource to this builder.
		 * @param f the file
		 * @return this builder
		 */
		public Builder add(AnnotatedFile f) {
			add(f, baseFolder.getPath().relativize(requireDescendant(baseFolder.getPath(), f.getPath()).normalize()).toString());
			return this;
		}

		/**
		 * Adds a resource to this builder.
		 * @param f the file
		 * @param path the path within the file set
		 * @return this builder
		 */
		public Builder add(Path f, String path) {
			add(DefaultAnnotatedFile.with(f).build(), path);
			return this;
		}

		/**
		 * Adds a resource to this builder.
		 * @param f the file
		 * @param path the path within the file set
		 * @return this builder
		 */
		public Builder add(AnnotatedFile f, String path) {
			resources.put(normalizeRelativePath(baseFolder.getPath(), path), f);
			return this;
		}

		/**
		 * Builds the file set.
		 * @return a new file set
		 */
		public DefaultFileSet build() {
			return new DefaultFileSet(this);
		}
	}

	private DefaultFileSet(Builder builder) {
		this.baseFolder = builder.baseFolder;
		this.manifestPath = builder.manifestPath;
		this.formatIdentifier = Optional.ofNullable(builder.formatIdentifier);
		this.resources = new HashMap<>(builder.resources);
	}

	/**
	 * Creates a new builder with the specified base folder and manifest.
	 * @param baseFolder the base folder
	 * @param manifest the manifest
	 * @return returns a new builder
	 * @throws IllegalArgumentException if the manifest isn't a descendant of the base folder
	 */
	public static DefaultFileSet.Builder with(BaseFolder baseFolder, AnnotatedFile manifest) {
		return new Builder(baseFolder, manifest);
	}
	
	/**
	 * Creates a new builder with the specified base folder and manifest.
	 * @param baseFolder the base folder
	 * @param manifest the manifest
	 * @param manifestPath the path to the manifest within the file set
	 * @return returns a new builder
	 * @throws IllegalArgumentException if the manifest isn't a descendant of the base folder
	 */
	public static DefaultFileSet.Builder with(BaseFolder baseFolder, AnnotatedFile manifest, String manifestPath) {
		return new Builder(baseFolder, manifest, manifestPath);
	}
	
	static String normalizeRelativePath(Path base, String path) {
		return base.relativize(base.resolve(path)).normalize().toString();
	}
	
	static Path requireDescendant(Path base, Path path) {
		if (!isDescendant(base, path)) {
			throw new IllegalArgumentException(String.format("'%s' is not a descendant of '%s'", path, base));
		}
		return path;
	}
	
	static boolean isDescendant(Path base, Path descendant) {
		return descendant.toAbsolutePath().normalize().startsWith(base.toAbsolutePath().normalize());
	}
	
	boolean isDescendant(Path descendant) {
		return isDescendant(getBaseFolder().getPath(), descendant);
	}

	@Override
	public BaseFolder getBaseFolder() {
		return baseFolder;
	}

	@Override
	public AnnotatedFile getManifest() {
		return Objects.requireNonNull(resources.get(manifestPath));
	}
	
	public String getManifestPath() {
		return manifestPath;
	}
	
	@Override
	public boolean isManifest(String path) {
		return resources.containsKey(normalizeRelativePath(baseFolder.getPath(), path));
	}

	@Override
	public Optional getFormatIdentifier() {
		return formatIdentifier;
	}
	
	@Override
	public Set getResourcePaths() {
		return Collections.unmodifiableSet(resources.keySet());
	}
	
	@Override
	public Optional getResourceForKey(String path) {
		return Optional.ofNullable(resources.get(path));
	}
	
	@Override
	public Optional getResource(String path) {
		return Optional.ofNullable(resources.get(normalizeRelativePath(baseFolder.getPath(), path)));
	}
	
	@Override
	public Optional getResource(Path path) {
		return getResource(getBaseFolder().getPath().relativize(path).toString());
	}

	/**
	 * Creates a new file set at the specified location. All other properties 
	 * are copied from this file set. Resources are copied to the new file set
	 * to the extent possible, see {@link #internalizeAllCopy()}.
	 * @param source the original file set
	 * @param target the new location, this must point to an existing directory. It is recommended,
	 * although not strictly required, that the folder is also empty.
	 * @return the created file set
	 * @throws IOException if an I/O error occurs
	 */
	public static DefaultFileSet copy(FileSet source, BaseFolder target) throws IOException {
		return copyInner(source, source.getManifestPath(), target);
	}

	/**
	 * Creates a new file set at the specified location. All other properties 
	 * are copied from this file set. Resources are copied to the new file set
	 * to the extent possible, see {@link #internalizeAllCopy()}.
	 * @param source the original file set
	 * @param target the new location, this must point to an existing directory. It is recommended,
	 * although not strictly required, that the folder is also empty.
	 * @param manifestFileName the name of the manifest file. Note that the path to the manifest file
	 * cannot be influenced
	 * @return the created file set
	 * @throws IOException if an I/O error occurs
	 * @throws IllegalArgumentException if the file name is not valid
	 */
	public static DefaultFileSet copy(FileSet source, BaseFolder target, String manifestFileName) throws IOException {
		Path manifestFolder = target.getPath().resolve(source.getManifestPath()).getParent();
		Path manifestPath = manifestFolder.resolve(manifestFileName);
		if (!manifestPath.getParent().equals(manifestFolder)) {
			throw new IllegalArgumentException("Illegal characters in file name: " + manifestFileName);
		}
		String manifestPathString = target.getPath()
				.relativize(manifestPath)
				.normalize()
				.toString();
		return copyInner(source, manifestPathString, target);
	}

	private static DefaultFileSet copyInner(FileSet source, String manifestPath, BaseFolder target) throws IOException {
		Files.createDirectories(target.getPath());
		// Create a new file set at the specified location
		// All other properties are copied
		DefaultFileSet.Builder builder = new DefaultFileSet.Builder(target, source.getManifest(), manifestPath);
		builder.formatIdentifier(source.getFormatIdentifier().orElse(null));
		// Add all resources from the original file set
		source.getResourcePaths().stream().forEach(v->{
			if (!v.equals(source.getManifestPath())) {
				source.getResourceForKey(v).ifPresent(key->builder.add(key, v));
			}
		});
		DefaultFileSet ret = builder.build();
		// Internalize all resources by coping them
		ret.internalizeAllCopy();
		return ret;
	}

	static Optional findCommonAncestor(Collection resources) {
		if (resources.isEmpty()) {
			return Optional.empty();
		}
		return findCommonAncestor(resources.stream());
	}

	private static Optional findCommonAncestor(Stream resources) {
		// Map to a set of normalized paths
		Set tmp = resources
				.map(v->v.normalize())
				.collect(Collectors.toSet());
		if (tmp.size()==1) {
			// Easy, only one resource
			return Optional.ofNullable(tmp.iterator().next().getParent());
		}
		if (tmp.stream().map(v->v.getRoot()).distinct().count()>1) {
			return Optional.empty();
		}
		int maxNameCount = tmp.stream()
				.mapToInt(v->v.getNameCount())
				.max()
				.orElse(0);
		// Look for shared ancestors
		int ret = 0;
		for (int i=1;;i++) {
			int ii = i;
			if (iv.subpath(0, Math.min(ii, v.getNameCount()-1)))
					.distinct()
					.count()==1) {
				ret = i;
			} else {
				break;
			}
		}
		if (ret==0) {
			return Optional.empty();
		} else {
			// All resources are equal up to the end index, so just use any
			Path p = tmp.iterator().next();
			Path sub = p.subpath(0, ret);
			// There's no reason for sub to be null here, but it doesn't hurt to check it
			if (sub==null) {
				return Optional.empty();
			} else {
				Path root = p.getRoot();
				return root==null ? Optional.of(sub) : Optional.of(root.resolve(sub));
			}
		}
	}
	
	@Override
	public Stream streamExternal() {
		return resources.values().stream()
			.filter(v->!isDescendant(baseFolder.getPath(), v.getPath()));
	}

	@Override
	public void internalizeAllCopy() {
		resources.entrySet().forEach(r->{
			internalizeCopy(r.getKey(), r.getValue()).ifPresent(f->r.setValue(f));
		});
	}
	
	@Override
	public void internalizeBelow(Path base) {
		resources.entrySet().forEach(r->{
			if (isDescendant(base, r.getValue().getPath())) {
				internalize(r.getKey(), r.getValue()).ifPresent(v->r.setValue(v));
			}
		});
	}

	@Override
	public boolean internalizeCopy(String path) {
		Optional res = Optional.ofNullable(resources.get(path)).flatMap(f->internalizeCopy(path, f));
		if (res.isPresent()) {
			resources.put(path, res.get());
			return true;
		}
		return false;
	}
	
	@Override
	public boolean internalize(String path) {
		Optional res = Optional.ofNullable(resources.get(path)).flatMap(v->internalize(path, v));
		if (res.isPresent()) {
			resources.put(path, res.get());
			return true;
		}
		return false;
	}
	
	private Optional internalizeCopy(String path, AnnotatedFile f) {
		if (!isDescendant(baseFolder.getPath(), f.getPath())) {
			Path newLocation = baseFolder.getPath().resolve(path);
			if (isDescendant(baseFolder.getPath(), newLocation)) {
				try {
					Files.createDirectories(newLocation.getParent());
					Files.copy(f.getPath(), newLocation, StandardCopyOption.REPLACE_EXISTING);
					return Optional.of(DefaultAnnotatedFile.with(f).file(newLocation).build());
				} catch (IOException e) {
					logger.log(Level.WARNING, "Could not copy file: " + f.getPath(), e);
				}				
			}
		}
		return Optional.empty();
	}
	
	private Optional internalize(String path, AnnotatedFile f) {
		if (!isDescendant(baseFolder.getPath(), f.getPath())) {
			Path newLocation = baseFolder.getPath().resolve(path);
			if (isDescendant(baseFolder.getPath(), newLocation)) {
				try {
					Files.createDirectories(newLocation.getParent());
					Files.move(f.getPath(), newLocation, StandardCopyOption.REPLACE_EXISTING);
					return Optional.of(DefaultAnnotatedFile.with(f).file(newLocation).build());
				} catch (IOException e) {
					logger.log(Level.WARNING, "Could not move file: " + f.getPath(), e);
				}
			}
		}
		return Optional.empty();
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy