org.tomitribe.util.dir.Dir Maven / Gradle / Ivy
/*
* 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.tomitribe.util.dir;
import org.tomitribe.util.Files;
import org.tomitribe.util.reflect.Generics;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Efforts to create strongly-typed code are often poisoned by string file path references
* spread all over the same codebase.
*
* This utility was born out of a desire that all path references could be compile-time checked and code completed.
*
* Step 1 Create an interface matching a directory
*
* For example, a directory structure like this:
*
*
* project/
* - src/
* - target/
* - .git/
* - pom.xml
*
* Could be handled with the following interface:
*
*
* public interface Project {
* File src();
* File target();
* @Name(".git")
* File git();
* @Name("pom.xml")
* File pomXml();
* }
*
* Step 2 Get a proxied reference to that directory
*
*
* File mydir = new File("/some/path/to/a/project");
* Project project = Dir.of(Project.class, mydir);
*
* Under the covers the interface is implemented as a dynamic proxy whose InvocationHandler is
* holding the actual File object.
*
* Returning Another Interface
*
* Instead of src()
returning a File
, it could return another similar interface. For example
*
*
* public interface Src {
* File main();
* File test();
* }
*
* And now we update Project
so the src()
method will return Src
*
* public interface Project {
* Src src();
* File target();
* @Name(".git")
* File git();
* @Name("pom.xml")
* File pomXml();
* }
*
*
* Now we have a strongly-typed directory structure that also supports code completion in the IDE.
*
* Passing a Subdirectory Name
*
* There may be times when you don't know the exact subdirectory name, but you know that it will use a specific
* directory structure. Here's how you might reference a nested Maven module structure:
*
* public interface Module {
* @Name("pom.xml")
* File pomXml();
* File src();
* File target();
* Module submodule(String name);
* }
*
*/
public interface Dir {
File dir();
File mkdir();
File mkdirs();
File get();
File parent();
File file(String name);
void delete();
static T of(final Class clazz, final File file) {
return (T) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[]{clazz},
new DirHandler(file)
);
}
class DirHandler implements InvocationHandler {
private final File dir;
public DirHandler(final File dir) {
this.dir = dir;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (method.isDefault()) {
return invokeDefault(proxy, method, args);
}
if (method.getDeclaringClass().equals(Dir.class)) {
if (method.getName().equals("dir")) return dir;
if (method.getName().equals("get")) return dir;
if (method.getName().equals("parent")) return dir.getParentFile();
if (method.getName().equals("mkdir")) return mkdir();
if (method.getName().equals("mkdirs")) return mkdirs();
if (method.getName().equals("delete")) return delete();
throw new IllegalStateException("Unknown method " + method);
}
final File file = new File(dir, name(method));
final Function action = action(method);
final Class> returnType = method.getReturnType();
if (returnType.isArray()) {
return returnArray(method);
}
if (Stream.class.equals(returnType) && args == null) {
return returnStream(method);
}
if (File.class.equals(returnType) && args == null) {
return returnFile(method, action.apply(file));
}
if (returnType.isInterface() && args != null && args.length == 1 && args[0] instanceof String) {
return Dir.of(returnType, action.apply(new File(dir, (String) args[0])));
}
if (returnType.isInterface() && args == null) {
return Dir.of(returnType, action.apply(file));
}
throw new UnsupportedOperationException(method.toGenericString());
}
private static Object invokeDefault(final Object proxy, final Method method, final Object[] args) throws Throwable {
final float version = Float.parseFloat(System.getProperty("java.class.version"));
if (version <= 52) { // Java 8
final Constructor constructor = Lookup.class.getDeclaredConstructor(Class.class);
constructor.setAccessible(true);
final Class> clazz = method.getDeclaringClass();
return constructor.newInstance(clazz)
.in(clazz)
.unreflectSpecial(method, clazz)
.bindTo(proxy)
.invokeWithArguments(args);
} else { // Java 9 and later
return MethodHandles.lookup()
.findSpecial(
method.getDeclaringClass(),
method.getName(),
MethodType.methodType(method.getReturnType(), new Class[0]),
method.getDeclaringClass()
).bindTo(proxy)
.invokeWithArguments(args);
}
}
private Object returnStream(final Method method) {
final Class returnType = (Class) Generics.getReturnType(method);
final Predicate filter = getFilter(method);
if (returnType.isInterface()) {
return stream(dir, method)
.filter(filter)
.map(child -> Dir.of(returnType, child));
}
if (File.class.equals(returnType)) {
return stream(dir, method)
.filter(filter);
}
throw new UnsupportedOperationException(method.toGenericString());
}
private Object returnFile(final Method method, final File file) throws FileNotFoundException {
// They want an exception if the file isn't found
if (exceptions(method).contains(FileNotFoundException.class) && !file.exists()) {
throw new FileNotFoundException(file.getAbsolutePath());
}
return file;
}
private Object returnArray(final Method method) {
final Predicate filter = getFilter(method);
final Class> arrayType = method.getReturnType().getComponentType();
if (File.class.equals(arrayType)) {
return stream(dir, method)
.filter(filter)
.toArray(File[]::new);
} else if (arrayType.isInterface()) {
// will be an array of type Object[]
final Object[] src = stream(dir, method)
.filter(filter)
.map(child -> Dir.of(arrayType, child))
.toArray();
// will be an array of the user's interface type
final Object[] dest = (Object[]) Array.newInstance(arrayType, src.length);
System.arraycopy(src, 0, dest, 0, src.length);
return dest;
}
throw new UnsupportedOperationException(method.toGenericString());
}
private static Stream stream(final File dir, final Method method) {
final Walk walk = method.getAnnotation(Walk.class);
if (walk != null) return walk(walk, dir);
return Stream.of(dir.listFiles());
}
private static Stream walk(final Walk walk, final File dir) {
try {
if (walk.maxDepth() != -1) {
return java.nio.file.Files.walk(dir.toPath(), walk.maxDepth())
.map(Path::toFile);
}
return java.nio.file.Files.walk(dir.toPath())
.map(Path::toFile);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private Predicate getFilter(final Method method) {
final Filter filter = method.getAnnotation(Filter.class);
if (filter == null) return pathname -> true;
final Class extends FileFilter> clazz = filter.value();
try {
final FileFilter fileFilter = clazz.newInstance();
return fileFilter::accept;
} catch (Exception e) {
throw new IllegalStateException("Unable to instantiate filter " + clazz, e);
}
}
private File mkdir() {
Files.mkdir(dir);
return dir;
}
private Void mkdirs() {
Files.mkdirs(dir);
return null;
}
private Void delete() {
Files.remove(dir);
return null;
}
private String name(final Method method) {
if (method.isAnnotationPresent(Name.class)) {
return method.getAnnotation(Name.class).value();
}
return method.getName();
}
public List> exceptions(final Method method) {
final Class>[] exceptionTypes = method.getExceptionTypes();
return Arrays.asList(exceptionTypes);
}
public Function action(final Method method) {
if (method.isAnnotationPresent(Mkdir.class)) return mkdir(method);
if (method.isAnnotationPresent(Mkdirs.class)) return mkdirs(method);
return noop(method);
}
public Function mkdir(final Method method) {
return file -> {
try {
Files.mkdir(file);
return file;
} catch (Exception e) {
throw new MkdirFailedException(method, file, e);
}
};
}
public Function mkdirs(final Method method) {
return file -> {
try {
Files.mkdirs(file);
return file;
} catch (Exception e) {
throw new MkdirsFailedException(method, file, e);
}
};
}
public Function noop(final Method method) {
return file -> file;
}
}
class MkdirFailedException extends RuntimeException {
public MkdirFailedException(final Method method, final File dir, final Throwable t) {
super(String.format("@Mkdir failed%n method: %s%n path: %s", method, dir.getAbsolutePath()), t);
}
}
class MkdirsFailedException extends RuntimeException {
public MkdirsFailedException(final Method method, final File dir, final Throwable t) {
super(String.format("@Mkdirs failed%n method: %s%n path: %s", method, dir.getAbsolutePath()), t);
}
}
}