ThemisReflectJar.java
/*
* Themis: Java Project Framework
* Copyright 2012-2026. Tony Washer
*
* Licensed 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 io.github.tonywasher.joceanus.themis.solver.reflect;
import com.github.javaparser.ast.body.BodyDeclaration;
import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.themis.exc.ThemisDataException;
import io.github.tonywasher.joceanus.themis.parser.ThemisParser;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisChar;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisClassInstance;
import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisTypeInstance;
import io.github.tonywasher.joceanus.themis.parser.proj.ThemisMavenId;
import io.github.tonywasher.joceanus.themis.parser.proj.ThemisProject;
import io.github.tonywasher.joceanus.themis.parser.type.ThemisTypeClassInterface;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Solve external class references via Jars and reflection.
*/
public class ThemisReflectJar
implements AutoCloseable {
/**
* The URL Jar prefix.
*/
private static final String URLJAR_PREFIX = "jar:file:/";
/**
* The URL Jar prefix.
*/
private static final String URLJAR_SUFFIX = "!/";
/**
* The Project parser.
*/
private final ThemisParser theProjectParser;
/**
* The JarClass Loader.
*/
private final URLClassLoader theClassLoader;
/**
* The External Classes map.
*/
private final Map<String, ThemisReflectExternal> theExternalClasses;
/**
* Constructor.
*
* @param pParser the project parser.
* @param pExternal classes the externalClass map.
* @throws OceanusException on error
*/
public ThemisReflectJar(final ThemisParser pParser,
final Map<String, ThemisReflectExternal> pExternal) throws OceanusException {
/* Create URL list and create URL Loader */
theProjectParser = pParser;
theExternalClasses = pExternal;
final URL[] myUrls = determineURLList(pParser.getProject());
theClassLoader = URLClassLoader.newInstance(myUrls);
}
/**
* Process external class list.
*
* @throws OceanusException on error
*/
public void processExternalClasses() throws OceanusException {
/* Extract the values as a separate list */
final List<ThemisReflectExternal> myExternals = new ArrayList<>(theExternalClasses.values());
/* Loop through the list */
for (ThemisReflectExternal myClass : myExternals) {
/* Load the external class */
final Class<?> myLoaded = loadClass(myClass.getFullName());
/* Create a resolved class based on the loaded class */
final BodyDeclaration<?> myResolved = buildClass(myLoaded);
final ThemisClassInstance myInstance = (ThemisClassInstance) theProjectParser.parseDeclaration(myResolved);
myClass.setClassInstance(myInstance);
/* Process ancestors */
processAncestors(myClass);
/* Process children */
processChildren(myLoaded.getClasses());
}
}
/**
* Try a class as a java.lang class.
*
* @param pName the class name
* @return the loaded class or null if it did not exist
*/
public ThemisReflectExternal tryJavaLang(final String pName) {
/* Create the javaLang name and try for the named class */
final String myFullName = ThemisReflectExternal.JAVALANG + pName;
return tryNamedClass(myFullName);
}
/**
* Try a class as a java.lang class.
*
* @param pName the class name
* @return the loaded class or null if it did not exist
*/
public ThemisReflectExternal tryNamedClass(final String pName) {
/* Protect against exceptions */
try {
/* Load the external class */
final ThemisReflectExternal myExternal = theExternalClasses.get(pName);
if (myExternal == null) {
return loadNamedClass(pName);
}
return myExternal;
/* If we failed, just return null */
} catch (OceanusException e) {
return null;
}
}
/**
* Load the class.
*
* @param pFullName the fullName of the class
* @return the loaded class
* @throws OceanusException on error
*/
private ThemisReflectExternal loadNamedClass(final String pFullName) throws OceanusException {
/* Load the external class */
final Class<?> myLoaded = loadClass(pFullName);
/* Create a resolved class based on the loaded class */
final BodyDeclaration<?> myResolved = buildClass(myLoaded);
final ThemisClassInstance myInstance = (ThemisClassInstance) theProjectParser.parseDeclaration(myResolved);
final ThemisReflectExternal myExternal = new ThemisReflectExternal(myInstance);
/* Store it as an external Class */
theExternalClasses.put(pFullName, myExternal);
/* Process ancestors */
processAncestors(myExternal);
/* Process children */
processChildren(myLoaded.getClasses());
/* return the class */
return myExternal;
}
/**
* Process external class list.
*
* @param pExternal the external classes.
* @throws OceanusException on error
*/
private void processAncestors(final ThemisReflectExternal pExternal) throws OceanusException {
/* Access aunderlying instance */
final ThemisClassInstance myInstance = pExternal.getClassInstance();
/* Process all the extended classes */
for (ThemisTypeInstance myAncestor : myInstance.getExtends()) {
/* Process the ancestor */
pExternal.addAncestor(processAncestor((ThemisTypeClassInterface) myAncestor));
}
/* Process all the implemented classes */
for (ThemisTypeInstance myAncestor : myInstance.getImplements()) {
/* Process the ancestor */
pExternal.addAncestor(processAncestor((ThemisTypeClassInterface) myAncestor));
}
}
/**
* Process an ancestor.
*
* @param pAncestor the ancestor.
* @return the resolved ancestor
* @throws OceanusException on error
*/
private ThemisReflectExternal processAncestor(final ThemisTypeClassInterface pAncestor) throws OceanusException {
/* Access the name of the class and convert to period format */
final String myFullName = pAncestor.getFullName().replace(ThemisChar.DOLLAR, ThemisChar.PERIOD);
/* See whether we have seen this class before */
ThemisReflectExternal myExternal = theExternalClasses.get(myFullName);
if (myExternal == null) {
/* Load the external class */
myExternal = loadNamedClass(myFullName);
}
/* Add link */
pAncestor.setClassInstance(myExternal);
return myExternal;
}
/**
* Process child classes.
*
* @param pChildren the children.
* @throws OceanusException on error
*/
private void processChildren(final Class<?>[] pChildren) throws OceanusException {
/* Loop through the children */
for (Class<?> myChild : pChildren) {
/* Ignore private/anonymous and local classes */
final boolean isLocalAnon = myChild.isAnonymousClass() || myChild.isLocalClass();
final boolean isPrivate = ThemisReflectBaseUtils.isPrivate(myChild.getModifiers());
if (!isPrivate && !isLocalAnon) {
processChild(myChild);
}
}
}
/**
* Process child class.
*
* @param pChild the child.
* @throws OceanusException on error
*/
private void processChild(final Class<?> pChild) throws OceanusException {
/* Create a resolved class based on the loaded class */
final BodyDeclaration<?> myResolved = buildClass(pChild);
final ThemisClassInstance myInstance = (ThemisClassInstance) theProjectParser.parseDeclaration(myResolved);
final ThemisReflectExternal myExternal = new ThemisReflectExternal(myInstance);
/* Store it as an external Class */
theExternalClasses.put(myExternal.getFullName(), myExternal);
/* Process ancestors */
processAncestors(myExternal);
}
/**
* determine the URL List.
*
* @param pProject the project
* @return the URL List
* @throws OceanusException on error
*/
private URL[] determineURLList(final ThemisProject pProject) throws OceanusException {
/* Create list of URLs for the dependencies */
final List<URL> myList = new ArrayList<>();
for (ThemisMavenId myId : pProject.getDependencies()) {
/* Protect against exceptions */
try {
final File myJar = myId.getMavenJarPath();
final String myName = myJar.toString().replace("\\", "/");
final String myPrefix = myName.startsWith("/") ? URLJAR_PREFIX + ThemisChar.COMMENT : URLJAR_PREFIX;
final URL myUrl = (new URI(myPrefix + myName + URLJAR_SUFFIX)).toURL();
myList.add(myUrl);
/* Handle exceptions */
} catch (URISyntaxException
| MalformedURLException e) {
throw new ThemisDataException("Failed to build URL", e);
}
}
/* Convert list to array */
return myList.toArray(new URL[0]);
}
/**
* Load a class.
*
* @param pClassName the class name.
* @return the loaded class
* @throws OceanusException on error
*/
private Class<?> loadClass(final String pClassName) throws OceanusException {
/* Protect against exceptions */
try {
return theClassLoader.loadClass(pClassName);
/* If we failed to find the class */
} catch (ClassNotFoundException e) {
/* Try again with the canonical name converted to a subClass */
final String mySubClass = trySubClass(pClassName);
if (mySubClass != null) {
return loadClass(mySubClass);
}
/* Failed to find the class */
throw new ThemisDataException("Failed to find class " + pClassName, e);
}
}
/**
* Change class name to make last class subClass.
*
* @param pClassName the class name
* @return the subClass name or null
*/
private static String trySubClass(final String pClassName) {
/* Swap last period for dollar */
final int myLastIndex = pClassName.lastIndexOf(ThemisChar.PERIOD);
return myLastIndex != -1
? pClassName.substring(0, myLastIndex) + ThemisChar.DOLLAR + pClassName.substring(myLastIndex + 1)
: null;
}
/**
* build class.
*
* @param pSource the source class
* @return the parsed class
* @throws OceanusException on error
*/
private BodyDeclaration<?> buildClass(final Class<?> pSource) throws OceanusException {
/* Build the relevant class type */
if (pSource.isAnnotation()) {
return new ThemisReflectAnnotation(pSource);
} else if (pSource.isEnum()) {
return new ThemisReflectEnum(pSource);
} else if (pSource.isRecord()) {
return new ThemisReflectRecord(pSource);
} else {
return new ThemisReflectClass(pSource);
}
}
@Override
public void close() {
try {
if (theClassLoader != null) {
theClassLoader.close();
}
} catch (IOException e) {
/* Do nothing */
}
}
}