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

matlabcontrol.link.InvocationInfo Maven / Gradle / Ivy

There is a newer version: 4.6.0
Show newest version
/*
 * Code licensed under new-style BSD (see LICENSE).
 * All code up to tags/original: Copyright (c) 2013, Joshua Kaplan
 * All code after tags/original: Copyright (c) 2016, DiffPlug
 */
package matlabcontrol.link;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * A holder of information about the Java method and the associated MATLAB function.
 * 
 * @since 4.2.0
 * @author Joshua Kaplan
 */
class InvocationInfo implements Serializable {
	private static final long serialVersionUID = 764281612994597133L;

	/**
	 * The name of the function.
	 */
	final String name;

	/**
	 * The directory containing the function. This is an absolute path to an m-file on the system, never inside of
	 * a compressed file such as a jar. {@code null} if the function is (supposed to be) on MATLAB's path.
	 */
	final String containingDirectory;

	/**
	 * The types of each returned argument. The length of this array is the nargout to call the function with.
	 */
	final Class[] returnTypes;

	/**
	 * Array of generic type parameters of the return types. For instance for return type at index i in
	 * {@code returnTypes} the generic parameters are in the array returned by {@code returnTypeGenericParameters}
	 * at index i.
	 */
	final Class[][] returnTypeParameters;

	static InvocationInfo getInvocationInfo(Method method, MatlabFunction annotation) {
		FunctionInfo funcInfo = getFunctionInfo(method, annotation);
		ReturnTypeInfo returnInfo = getReturnTypes(method);
		InvocationInfo invocationInfo = new InvocationInfo(funcInfo.name, funcInfo.containingDirectory,
				returnInfo.returnTypes, returnInfo.returnTypeParameters);

		return invocationInfo;
	}

	InvocationInfo(String name, String containingDirectory, Class[] returnTypes, Class[][] returnTypeParameters) {
		this.name = name;
		this.containingDirectory = containingDirectory;

		this.returnTypes = returnTypes;
		this.returnTypeParameters = returnTypeParameters;
	}

	@Override
	public String toString() {
		StringBuilder genericParameters = new StringBuilder();
		genericParameters.append("[");
		for (int i = 0; i < returnTypeParameters.length; i++) {
			genericParameters.append(classArrayToString(returnTypeParameters[i]));

			if (i != returnTypeParameters.length - 1) {
				genericParameters.append(" ");
			}
		}
		genericParameters.append("]");

		return "[" + this.getClass().getSimpleName() +
				" name=" + name + "," +
				" containingDirectory=" + containingDirectory + "," +
				" returnTypes=" + classArrayToString(returnTypes) + "," +
				" returnTypesGenericParameters=" + genericParameters + "]";
	}

	private static String classArrayToString(Class[] array) {
		StringBuilder str = new StringBuilder();
		str.append("[");
		for (int i = 0; i < array.length; i++) {
			str.append(array[i].getCanonicalName());

			if (i != array.length - 1) {
				str.append(" ");
			}
		}
		str.append("]");
		return str.toString();
	}

	private static class FunctionInfo {
		final String name;
		final String containingDirectory;

		public FunctionInfo(String name, String containingDirectory) {
			this.name = name;
			this.containingDirectory = containingDirectory;
		}
	}

	private static FunctionInfo getFunctionInfo(Method method, MatlabFunction annotation) {
		//Determine the function's name and if applicable, the directory the function is located in
		String functionName;
		String containingDirectory;

		//If a function name
		if (isFunctionName(annotation.value())) {
			functionName = annotation.value();
			containingDirectory = null;
		}
		//If a path
		else {
			String path = annotation.value();

			//Retrieve location of function file
			File functionFile;
			if (new File(path).isAbsolute()) {
				functionFile = new File(path);
			} else {
				functionFile = resolveRelativePath(method, path);
			}

			//Resolve canonical path
			try {
				functionFile = functionFile.getCanonicalFile();
			} catch (IOException e) {
				throw new LinkingException("Unable to resolve canonical path of specified function\n" +
						"method: " + method.getName() + "\n" +
						"path:" + path + "\n" +
						"non-canonical path: " + functionFile.getAbsolutePath(), e);
			}

			//Validate file location
			if (!functionFile.exists()) {
				throw new LinkingException("Specified file does not exist\n" +
						"method: " + method.getName() + "\n" +
						"path: " + path + "\n" +
						"resolved as: " + functionFile.getAbsolutePath());
			}
			if (!functionFile.isFile()) {
				throw new LinkingException("Specified file is not a file\n" +
						"method: " + method.getName() + "\n" +
						"path: " + path + "\n" +
						"resolved as: " + functionFile.getAbsolutePath());
			}
			if (!(functionFile.getName().endsWith(".m") || functionFile.getName().endsWith(".p"))) {
				throw new LinkingException("Specified file does not end in .m or .p\n" +
						"method: " + method.getName() + "\n" +
						"path: " + path + "\n" +
						"resolved as: " + functionFile.getAbsolutePath());
			}

			//Parse out the name of the function and the directory containing it
			containingDirectory = functionFile.getParent();
			functionName = functionFile.getName().substring(0, functionFile.getName().length() - 2);

			//Validate the function name
			if (!isFunctionName(functionName)) {
				throw new LinkingException("Specified file's name is not a MATLAB function name\n" +
						"Function Name: " + functionName + "\n" +
						"File: " + functionFile.getAbsolutePath());
			}
		}

		return new FunctionInfo(functionName, containingDirectory);
	}

	private static boolean isFunctionName(String functionName) {
		boolean isFunctionName = true;

		char[] nameChars = functionName.toCharArray();

		if (!Character.isLetter(nameChars[0])) {
			isFunctionName = false;
		} else {
			for (char element : nameChars) {
				if (!(Character.isLetter(element) || Character.isDigit(element) || element == '_')) {
					isFunctionName = false;
					break;
				}
			}
		}

		return isFunctionName;
	}

	/**
	 * Cache used by {@link #resolveRelativePath(Method, String)}
	 */
	private static final ConcurrentHashMap, Map> UNZIPPED_ENTRIES = new ConcurrentHashMap, Map>();

	/**
	 * Resolves the location of {@code relativePath} relative to the interface which declared {@code method}. If the
	 * interface is inside of a zip file (jar/war/ear etc. file) then the contents of the zip file may first need to be
	 * unzipped.
	 * 
	 * @param method
	 * @param relativePath
	 * @return the absolute location of the file
	 */
	private static File resolveRelativePath(Method method, String relativePath) {
		Class theInterface = method.getDeclaringClass();
		File interfaceLocation = getClassLocation(theInterface);

		File functionFile;

		//Code source is in a file, which means it should be jar/ear/war/zip etc.
		if (interfaceLocation.isFile()) {
			if (!UNZIPPED_ENTRIES.containsKey(theInterface)) {
				UnzipResult unzipResult = unzip(interfaceLocation);
				Map mapping = unzipResult.unzippedMapping;
				List filesToDelete = new ArrayList(mapping.values());
				filesToDelete.add(unzipResult.rootDirectory);

				//If there was a previous mapping, delete all of the files just unzipped 
				if (UNZIPPED_ENTRIES.putIfAbsent(theInterface, mapping) != null) {
					deleteFiles(filesToDelete, true);
				}
				//No previous mapping, delete unzipped files on JVM exit
				else {
					deleteFiles(filesToDelete, false);
				}
			}

			functionFile = UNZIPPED_ENTRIES.get(theInterface).get(relativePath);

			if (functionFile == null) {
				throw new LinkingException("Unable to find file inside of zip\n" +
						"Method: " + method.getName() + "\n" +
						"Relative Path: " + relativePath + "\n" +
						"Zip File: " + interfaceLocation.getAbsolutePath());
			}
		}
		//Code source is a directory, it code should not be inside a jar/ear/war/zip etc.
		else {
			functionFile = new File(interfaceLocation, relativePath);
		}

		return functionFile;
	}

	private static void deleteFiles(Collection files, boolean deleteNow) {
		ArrayList sortedFiles = new ArrayList(files);
		Collections.sort(sortedFiles);

		//Delete files in the opposite order so that files are deleted before their containing directories
		if (deleteNow) {
			for (int i = sortedFiles.size() - 1; i >= 0; i--) {
				boolean deleted = sortedFiles.get(i).delete();
				if (!deleted) {
					System.err.println("unable to delete " + sortedFiles.get(i));
				}
			}
		}
		//Delete files in the existing order because the files will be deleted on exit in the opposite order
		else {
			for (File file : sortedFiles) {
				file.deleteOnExit();
			}
		}
	}

	/**
	 * Cache used by {@link #getClassLocation(java.lang.Class)}}.
	 */
	private static final ConcurrentHashMap, File> CLASS_LOCATIONS = new ConcurrentHashMap, File>();

	private static File getClassLocation(Class clazz) {
		File result = CLASS_LOCATIONS.get(clazz);
		if (result != null) {
			return result;
		} else {
			try {
				URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
				File file = new File(url.toURI().getPath()).getCanonicalFile();
				if (!file.exists()) {
					throw new LinkingException("Incorrectly resolved location of class\n" +
							"class: " + clazz.getCanonicalName() + "\n" +
							"location: " + file.getAbsolutePath());
				}
				return CLASS_LOCATIONS.putIfAbsent(clazz, file);
			} catch (IOException e) {
				throw new LinkingException("Unable to determine location of " + clazz.getCanonicalName(), e);
			} catch (URISyntaxException e) {
				throw new LinkingException("Unable to determine location of " + clazz.getCanonicalName(), e);
			}
		}
	}

	private static class UnzipResult {
		final Map unzippedMapping;
		File rootDirectory;

		private UnzipResult(Map mapping, File root) {
			this.unzippedMapping = mapping;
			this.rootDirectory = root;
		}
	}

	/**
	 * Unzips the file located at {@code zipLocation}.
	 * 
	 * @param zipLocation the location of the file zip
	 * @return resulting files from unzipping
	 * @throws LinkingException if unable to unzip the zip file for any reason
	 */
	private static UnzipResult unzip(File zipLocation) {
		ZipFile zip;
		try {
			zip = new ZipFile(zipLocation);
		} catch (IOException e) {
			throw new LinkingException("Unable to open zip file\n" +
					"zip location: " + zipLocation.getAbsolutePath(), e);
		}

		try {
			//Mapping from entry names to the unarchived location on disk
			Map entryMap = new HashMap();

			//Destination
			File unzipDir = new File(System.getProperty("java.io.tmpdir"), "linked_" + UUID.randomUUID().toString());

			for (Enumeration entries = zip.entries(); entries.hasMoreElements();) {
				ZipEntry entry = entries.nextElement();

				//Directory
				if (entry.isDirectory()) {
					File destDir = new File(unzipDir, entry.getName());
					boolean mkdir = destDir.mkdirs();
					if (!mkdir) {
						System.err.println("Unable to mkdir " + destDir);
					}

					entryMap.put(entry.getName(), destDir);
				}
				//File
				else {
					//File should not exist, but confirm it
					File destFile = new File(unzipDir, entry.getName());
					if (destFile.exists()) {
						throw new LinkingException("Cannot unzip file, randomly generated path already exists\n" +
								"generated path: " + destFile.getAbsolutePath() + "\n" +
								"zip file: " + zipLocation.getAbsolutePath());
					}
					boolean mkdir = destFile.getParentFile().mkdirs();
					if (!mkdir) {
						System.err.println("Unable to mkdir " + destFile.getParentFile());
					}

					//Unarchive
					try {
						final int BUFFER_SIZE = 2048;
						OutputStream dest = new BufferedOutputStream(new FileOutputStream(destFile), BUFFER_SIZE);
						try {
							InputStream entryStream = zip.getInputStream(entry);
							try {
								byte[] buffer = new byte[BUFFER_SIZE];
								int count;
								while ((count = entryStream.read(buffer, 0, BUFFER_SIZE)) != -1) {
									dest.write(buffer, 0, count);
								}
								dest.flush();
							} finally {
								entryStream.close();
							}
						} finally {
							dest.close();
						}
					} catch (IOException e) {
						throw new LinkingException("Unable to unzip file entry\n" +
								"entry: " + entry.getName() + "\n" +
								"zip location: " + zipLocation.getAbsolutePath() + "\n" +
								"destination file: " + destFile.getAbsolutePath(), e);
					}

					entryMap.put(entry.getName(), destFile);
				}
			}

			return new UnzipResult(Collections.unmodifiableMap(entryMap), unzipDir);
		} finally {
			try {
				zip.close();
			} catch (IOException ex) {
				throw new LinkingException("Unable to close zip file: " + zipLocation.getAbsolutePath(), ex);
			}
		}
	}

	private static class ReturnTypeInfo {
		Class[] returnTypes;
		Class[][] returnTypeParameters;

		private ReturnTypeInfo(Class[] returnTypes, Class[][] returnTypeParameters) {
			this.returnTypes = returnTypes;
			this.returnTypeParameters = returnTypeParameters;
		}
	}

	private static ReturnTypeInfo getReturnTypes(Method method) {
		//The type-erasured return type of the method
		Class methodReturn = method.getReturnType();

		//The return type of the method with type information
		Type genericReturn = method.getGenericReturnType();

		//The return types to be determined
		Class[] returnTypes;
		Class[][] returnTypeGenericParameters;

		//0 return arguments
		if (methodReturn.equals(void.class)) {
			returnTypes = new Class[0];
			returnTypeGenericParameters = new Class[0][0];
		}
		//1 return argument
		else if (!MatlabReturns.ReturnN.class.isAssignableFrom(methodReturn)) {
			//MatlabNumberArray subclasses are allowed to be parameterized
			if (MatlabNumberArray.class.isAssignableFrom(methodReturn)) {
				if (genericReturn instanceof ParameterizedType) {
					Type[] parameterizedTypes = ((ParameterizedType) genericReturn).getActualTypeArguments();
					Class parameter = getMatlabNumberArrayParameter(parameterizedTypes[0], methodReturn, method);
					returnTypeGenericParameters = new Class[][]{{parameter}};
				} else {
					throw new LinkingException(method + " must parameterize " + methodReturn.getCanonicalName());
				}
			} else if (!methodReturn.equals(genericReturn)) {
				throw new LinkingException(method + " may not have a return type that uses generics");
			} else {
				returnTypeGenericParameters = new Class[1][0];
			}

			returnTypes = new Class[]{methodReturn};
		}
		//2 or more return arguments
		else {
			if (genericReturn instanceof ParameterizedType) {
				Type[] parameterizedTypes = ((ParameterizedType) genericReturn).getActualTypeArguments();
				returnTypes = new Class[parameterizedTypes.length];
				returnTypeGenericParameters = new Class[parameterizedTypes.length][];

				for (int i = 0; i < parameterizedTypes.length; i++) {
					Type type = parameterizedTypes[i];

					if (type instanceof Class) {
						Class returnType = (Class) type;
						if (MatlabNumberArray.class.isAssignableFrom(returnType)) {
							throw new LinkingException(method + " must parameterize " + returnType.getCanonicalName());
						}
						returnTypes[i] = returnType;
						returnTypeGenericParameters[i] = new Class[0];
					} else if (type instanceof GenericArrayType) {
						returnTypes[i] = getClassOfArrayType((GenericArrayType) type, method);
						returnTypeGenericParameters[i] = new Class[0];
					} else if (type instanceof ParameterizedType) {
						//MatlabNumberArray subclasses are allowed to be parameterized
						ParameterizedType parameterizedType = (ParameterizedType) type;
						Type rawType = parameterizedType.getRawType();
						if (rawType instanceof Class && MatlabNumberArray.class.isAssignableFrom((Class) rawType)) {
							Class returnType = (Class) rawType;
							returnTypes[i] = returnType;

							Class parameter = getMatlabNumberArrayParameter(
									parameterizedType.getActualTypeArguments()[0], returnType, method);
							returnTypeGenericParameters[i] = new Class[]{parameter};
						} else {
							throw new LinkingException(method + " may not parameterize " +
									methodReturn.getCanonicalName() + " with a parameterized type");
						}
					} else if (type instanceof WildcardType) {
						throw new LinkingException(method + " may not parameterize " + methodReturn.getCanonicalName() +
								" with a wild card type");
					} else if (type instanceof TypeVariable) {
						throw new LinkingException(method + " may not parameterize " + methodReturn.getCanonicalName() +
								" with a generic type");
					} else {
						throw new LinkingException(method + " may not parameterize " + methodReturn.getCanonicalName() +
								" with " + type);
					}
				}
			} else {
				throw new LinkingException(method + " must parameterize " + methodReturn.getCanonicalName());
			}
		}

		return new ReturnTypeInfo(returnTypes, returnTypeGenericParameters);
	}

	private static Class getMatlabNumberArrayParameter(Type type, Class matlabArrayClass, Method method) {
		Class parameter;
		if (type instanceof GenericArrayType) {
			parameter = getClassOfArrayType((GenericArrayType) type, method);
			ClassInfo paramInfo = ClassInfo.getInfo(parameter);

			boolean validParameter = ((matlabArrayClass.equals(MatlabInt8Array.class) && byte.class.equals(paramInfo.baseComponentType)) ||
					(matlabArrayClass.equals(MatlabInt16Array.class) && short.class.equals(paramInfo.baseComponentType)) ||
					(matlabArrayClass.equals(MatlabInt32Array.class) && int.class.equals(paramInfo.baseComponentType)) ||
					(matlabArrayClass.equals(MatlabInt64Array.class) && long.class.equals(paramInfo.baseComponentType)) ||
					(matlabArrayClass.equals(MatlabSingleArray.class) && float.class.equals(paramInfo.baseComponentType)) ||
					(matlabArrayClass.equals(MatlabDoubleArray.class) && double.class.equals(paramInfo.baseComponentType)));

			if (!validParameter) {
				throw new LinkingException(method + " may not parameterize " + matlabArrayClass.getCanonicalName() +
						" with " + parameter.getCanonicalName());
			}
		} else {
			throw new LinkingException(method + " may not parameterize " + matlabArrayClass.getCanonicalName() +
					" with " + type);
		}

		return parameter;
	}

	/**
	 * 
	 * @param type
	 * @param method used for exception message only
	 * @return 
	 */
	private static Class getClassOfArrayType(GenericArrayType type, Method method) {
		int dimensions = 1;
		Type componentType = type.getGenericComponentType();
		while (!(componentType instanceof Class)) {
			dimensions++;
			if (componentType instanceof GenericArrayType) {
				componentType = ((GenericArrayType) componentType).getGenericComponentType();
			} else if (componentType instanceof TypeVariable) {
				throw new LinkingException(method + " may not parameterize " +
						method.getReturnType().getCanonicalName() + " with a generic array");
			} else {
				throw new LinkingException(method + " may not parameterize " +
						method.getReturnType().getCanonicalName() + " with an array of type " + type);
			}
		}

		return ArrayUtils.getArrayClass((Class) componentType, dimensions);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy