View Javadoc
1   /*
2    * Themis: Java Project Framework
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.themis.solver.reflect;
18  
19  import com.github.javaparser.ast.body.BodyDeclaration;
20  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
21  import io.github.tonywasher.joceanus.themis.exc.ThemisDataException;
22  import io.github.tonywasher.joceanus.themis.parser.ThemisParser;
23  import io.github.tonywasher.joceanus.themis.parser.base.ThemisChar;
24  import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisClassInstance;
25  import io.github.tonywasher.joceanus.themis.parser.base.ThemisInstance.ThemisTypeInstance;
26  import io.github.tonywasher.joceanus.themis.parser.proj.ThemisMavenId;
27  import io.github.tonywasher.joceanus.themis.parser.proj.ThemisProject;
28  import io.github.tonywasher.joceanus.themis.parser.type.ThemisTypeClassInterface;
29  
30  import java.io.File;
31  import java.io.IOException;
32  import java.net.MalformedURLException;
33  import java.net.URI;
34  import java.net.URISyntaxException;
35  import java.net.URL;
36  import java.net.URLClassLoader;
37  import java.util.ArrayList;
38  import java.util.List;
39  import java.util.Map;
40  
41  /**
42   * Solve external class references via Jars and reflection.
43   */
44  public class ThemisReflectJar
45          implements AutoCloseable {
46      /**
47       * The URL Jar prefix.
48       */
49      private static final String URLJAR_PREFIX = "jar:file:/";
50  
51      /**
52       * The URL Jar prefix.
53       */
54      private static final String URLJAR_SUFFIX = "!/";
55  
56      /**
57       * The Project parser.
58       */
59      private final ThemisParser theProjectParser;
60  
61      /**
62       * The JarClass Loader.
63       */
64      private final URLClassLoader theClassLoader;
65  
66      /**
67       * The External Classes map.
68       */
69      private final Map<String, ThemisReflectExternal> theExternalClasses;
70  
71      /**
72       * Constructor.
73       *
74       * @param pParser   the project parser.
75       * @param pExternal classes the externalClass map.
76       * @throws OceanusException on error
77       */
78      public ThemisReflectJar(final ThemisParser pParser,
79                              final Map<String, ThemisReflectExternal> pExternal) throws OceanusException {
80          /* Create URL list and create URL Loader */
81          theProjectParser = pParser;
82          theExternalClasses = pExternal;
83          final URL[] myUrls = determineURLList(pParser.getProject());
84          theClassLoader = URLClassLoader.newInstance(myUrls);
85      }
86  
87      /**
88       * Process external class list.
89       *
90       * @throws OceanusException on error
91       */
92      public void processExternalClasses() throws OceanusException {
93          /* Extract the values as a separate list */
94          final List<ThemisReflectExternal> myExternals = new ArrayList<>(theExternalClasses.values());
95  
96          /* Loop through the list */
97          for (ThemisReflectExternal myClass : myExternals) {
98              /* Load the external class */
99              final Class<?> myLoaded = loadClass(myClass.getFullName());
100 
101             /* Create a resolved class based on the loaded class */
102             final BodyDeclaration<?> myResolved = buildClass(myLoaded);
103             final ThemisClassInstance myInstance = (ThemisClassInstance) theProjectParser.parseDeclaration(myResolved);
104             myClass.setClassInstance(myInstance);
105 
106             /* Process ancestors */
107             processAncestors(myClass);
108 
109             /* Process children */
110             processChildren(myLoaded.getClasses());
111         }
112     }
113 
114     /**
115      * Try a class as a java.lang class.
116      *
117      * @param pName the class name
118      * @return the loaded class or null if it did not exist
119      */
120     public ThemisReflectExternal tryJavaLang(final String pName) {
121         /* Create the javaLang name and try for the named class */
122         final String myFullName = ThemisReflectExternal.JAVALANG + pName;
123         return tryNamedClass(myFullName);
124     }
125 
126     /**
127      * Try a class as a java.lang class.
128      *
129      * @param pName the class name
130      * @return the loaded class or null if it did not exist
131      */
132     public ThemisReflectExternal tryNamedClass(final String pName) {
133         /* Protect against exceptions */
134         try {
135             /* Load the external class */
136             final ThemisReflectExternal myExternal = theExternalClasses.get(pName);
137             if (myExternal == null) {
138                 return loadNamedClass(pName);
139             }
140             return myExternal;
141 
142             /* If we failed, just return null */
143         } catch (OceanusException e) {
144             return null;
145         }
146     }
147 
148     /**
149      * Load the class.
150      *
151      * @param pFullName the fullName of the class
152      * @return the loaded class
153      * @throws OceanusException on error
154      */
155     private ThemisReflectExternal loadNamedClass(final String pFullName) throws OceanusException {
156         /* Load the external class */
157         final Class<?> myLoaded = loadClass(pFullName);
158 
159         /* Create a resolved class based on the loaded class */
160         final BodyDeclaration<?> myResolved = buildClass(myLoaded);
161         final ThemisClassInstance myInstance = (ThemisClassInstance) theProjectParser.parseDeclaration(myResolved);
162         final ThemisReflectExternal myExternal = new ThemisReflectExternal(myInstance);
163 
164         /* Store it as an external Class */
165         theExternalClasses.put(pFullName, myExternal);
166 
167         /* Process ancestors */
168         processAncestors(myExternal);
169 
170         /* Process children */
171         processChildren(myLoaded.getClasses());
172 
173         /* return the class */
174         return myExternal;
175     }
176 
177     /**
178      * Process external class list.
179      *
180      * @param pExternal the external classes.
181      * @throws OceanusException on error
182      */
183     private void processAncestors(final ThemisReflectExternal pExternal) throws OceanusException {
184         /* Access aunderlying instance */
185         final ThemisClassInstance myInstance = pExternal.getClassInstance();
186 
187         /* Process all the extended classes */
188         for (ThemisTypeInstance myAncestor : myInstance.getExtends()) {
189             /* Process the ancestor */
190             pExternal.addAncestor(processAncestor((ThemisTypeClassInterface) myAncestor));
191         }
192 
193         /* Process all the implemented classes */
194         for (ThemisTypeInstance myAncestor : myInstance.getImplements()) {
195             /* Process the ancestor */
196             pExternal.addAncestor(processAncestor((ThemisTypeClassInterface) myAncestor));
197         }
198     }
199 
200     /**
201      * Process an ancestor.
202      *
203      * @param pAncestor the ancestor.
204      * @return the resolved ancestor
205      * @throws OceanusException on error
206      */
207     private ThemisReflectExternal processAncestor(final ThemisTypeClassInterface pAncestor) throws OceanusException {
208         /* Access the name of the class and convert to period format */
209         final String myFullName = pAncestor.getFullName().replace(ThemisChar.DOLLAR, ThemisChar.PERIOD);
210 
211         /* See whether we have seen this class before */
212         ThemisReflectExternal myExternal = theExternalClasses.get(myFullName);
213         if (myExternal == null) {
214             /* Load the external class */
215             myExternal = loadNamedClass(myFullName);
216         }
217 
218         /* Add link */
219         pAncestor.setClassInstance(myExternal);
220         return myExternal;
221     }
222 
223     /**
224      * Process child classes.
225      *
226      * @param pChildren the children.
227      * @throws OceanusException on error
228      */
229     private void processChildren(final Class<?>[] pChildren) throws OceanusException {
230         /* Loop through the children */
231         for (Class<?> myChild : pChildren) {
232             /* Ignore private/anonymous and local classes */
233             final boolean isLocalAnon = myChild.isAnonymousClass() || myChild.isLocalClass();
234             final boolean isPrivate = ThemisReflectBaseUtils.isPrivate(myChild.getModifiers());
235             if (!isPrivate && !isLocalAnon) {
236                 processChild(myChild);
237             }
238         }
239     }
240 
241     /**
242      * Process child class.
243      *
244      * @param pChild the child.
245      * @throws OceanusException on error
246      */
247     private void processChild(final Class<?> pChild) throws OceanusException {
248         /* Create a resolved class based on the loaded class */
249         final BodyDeclaration<?> myResolved = buildClass(pChild);
250         final ThemisClassInstance myInstance = (ThemisClassInstance) theProjectParser.parseDeclaration(myResolved);
251         final ThemisReflectExternal myExternal = new ThemisReflectExternal(myInstance);
252 
253         /* Store it as an external Class */
254         theExternalClasses.put(myExternal.getFullName(), myExternal);
255 
256         /* Process ancestors */
257         processAncestors(myExternal);
258     }
259 
260     /**
261      * determine the URL List.
262      *
263      * @param pProject the project
264      * @return the URL List
265      * @throws OceanusException on error
266      */
267     private URL[] determineURLList(final ThemisProject pProject) throws OceanusException {
268         /* Create list of URLs for the dependencies */
269         final List<URL> myList = new ArrayList<>();
270         for (ThemisMavenId myId : pProject.getDependencies()) {
271             /* Protect against exceptions */
272             try {
273                 final File myJar = myId.getMavenJarPath();
274                 final String myName = myJar.toString().replace("\\", "/");
275                 final String myPrefix = myName.startsWith("/") ? URLJAR_PREFIX + ThemisChar.COMMENT : URLJAR_PREFIX;
276                 final URL myUrl = (new URI(myPrefix + myName + URLJAR_SUFFIX)).toURL();
277                 myList.add(myUrl);
278 
279                 /* Handle exceptions */
280             } catch (URISyntaxException
281                      | MalformedURLException e) {
282                 throw new ThemisDataException("Failed to build URL", e);
283             }
284         }
285 
286         /* Convert list to array */
287         return myList.toArray(new URL[0]);
288     }
289 
290     /**
291      * Load a class.
292      *
293      * @param pClassName the class name.
294      * @return the loaded class
295      * @throws OceanusException on error
296      */
297     private Class<?> loadClass(final String pClassName) throws OceanusException {
298         /* Protect against exceptions */
299         try {
300             return theClassLoader.loadClass(pClassName);
301 
302             /* If we failed to find the class */
303         } catch (ClassNotFoundException e) {
304             /* Try again with the canonical name converted to a subClass */
305             final String mySubClass = trySubClass(pClassName);
306             if (mySubClass != null) {
307                 return loadClass(mySubClass);
308             }
309 
310             /* Failed to find the class */
311             throw new ThemisDataException("Failed to find class " + pClassName, e);
312         }
313     }
314 
315     /**
316      * Change class name to make last class subClass.
317      *
318      * @param pClassName the class name
319      * @return the subClass name or null
320      */
321     private static String trySubClass(final String pClassName) {
322         /* Swap last period for dollar */
323         final int myLastIndex = pClassName.lastIndexOf(ThemisChar.PERIOD);
324         return myLastIndex != -1
325                 ? pClassName.substring(0, myLastIndex) + ThemisChar.DOLLAR + pClassName.substring(myLastIndex + 1)
326                 : null;
327     }
328 
329     /**
330      * build class.
331      *
332      * @param pSource the source class
333      * @return the parsed class
334      * @throws OceanusException on error
335      */
336     private BodyDeclaration<?> buildClass(final Class<?> pSource) throws OceanusException {
337         /* Build the relevant class type */
338         if (pSource.isAnnotation()) {
339             return new ThemisReflectAnnotation(pSource);
340         } else if (pSource.isEnum()) {
341             return new ThemisReflectEnum(pSource);
342         } else if (pSource.isRecord()) {
343             return new ThemisReflectRecord(pSource);
344         } else {
345             return new ThemisReflectClass(pSource);
346         }
347     }
348 
349     @Override
350     public void close() {
351         try {
352             if (theClassLoader != null) {
353                 theClassLoader.close();
354             }
355         } catch (IOException e) {
356             /* Do nothing */
357         }
358     }
359 }