27
27
***** END LICENSE BLOCK *****/
28
28
package org .jruby .runtime .load ;
29
29
30
+ import jnr .posix .JavaSecuredFile ;
30
31
import org .jruby .Ruby ;
31
32
33
+ import java .net .URL ;
34
+
32
35
/**
33
36
* The ClassExtensionLibrary wraps a class which implements BasicLibraryService,
34
37
* and when asked to load the service, does a basicLoad of the BasicLibraryService.
@@ -41,40 +44,121 @@ public class ClassExtensionLibrary implements Library {
41
44
private final Class theClass ;
42
45
private final String name ;
43
46
47
+ /**
48
+ * Try to locate an extension service in the current classloader resources. This happens
49
+ * after the jar has been added to JRuby's URLClassLoader (JRubyClassLoader) and is how
50
+ * extensions can magically load.
51
+ *
52
+ * The basic logic is to use the require name (@param searchName) to build the name of
53
+ * a class ending in "Service", and then invoke that class to boot the extension.
54
+ *
55
+ * The looping logic here was in response to a RubyGems change that started absolutizing
56
+ * the path to the extension jar under some circumstances, leading to an incorrect
57
+ * package/class name for the service contained therein. The new logic will try
58
+ * successively more trailing elements until one of them is not a valid package
59
+ * name or all elements have been exhausted.
60
+ *
61
+ * @param runtime the current JRuby runtime
62
+ * @param searchName the name passed to `require`
63
+ * @return a ClassExtensionLibrary that will boot the ext, or null if none was found
64
+ */
44
65
static ClassExtensionLibrary tryFind (Ruby runtime , String searchName ) {
45
- // Create package name, by splitting on / and joining all but the last elements with a ".", and downcasing them.
46
- String [] all = searchName .split ("/" );
47
-
48
- StringBuilder finName = new StringBuilder ();
49
- for (int i =0 , j =(all .length -1 ); i <j ; i ++) {
50
- finName .append (all [i ].toLowerCase ()).append ("." );
51
- }
52
-
53
- try {
54
- // Make the class name look nice, by splitting on _ and capitalize each segment, then joining
55
- // the, together without anything separating them, and last put on "Service" at the end.
56
- String [] last = all [all .length -1 ].split ("_" );
57
- for (int i =0 , j =last .length ; i <j ; i ++) {
58
- if ("" .equals (last [i ])) break ;
59
- finName .append (Character .toUpperCase (last [i ].charAt (0 ))).append (last [i ].substring (1 ));
66
+ // Create package name, by splitting on / and successively accumulating elements to form a class name
67
+ String [] elts = searchName .split ("/" );
68
+
69
+ boolean isAbsolute = new JavaSecuredFile (searchName ).isAbsolute ();
70
+
71
+ String simpleName = buildSimpleName (elts [elts .length - 1 ]);
72
+
73
+ int firstElement = isAbsolute ? elts .length - 1 : 0 ;
74
+ for (; firstElement >= 0 ; firstElement --) {
75
+ String className = buildClassName (elts , firstElement , elts .length - 1 , simpleName );
76
+ ClassExtensionLibrary library = tryServiceLoad (runtime , className );
77
+
78
+ if (library != null ) return library ;
79
+ }
80
+
81
+ return null ;
82
+ }
83
+
84
+ /**
85
+ * Build the "simple" part of a service class name from the given require path element
86
+ * by splitting on "_" and concatenating as CamelCase, plus "Service" suffix.
87
+ *
88
+ * @param element the element from which to build a simple service class name
89
+ * @return the resulting simple service class name
90
+ */
91
+ private static String buildSimpleName (String element ) {
92
+ StringBuilder nameBuilder = new StringBuilder (element .length () + "Service" .length ());
93
+
94
+ String [] last = element .split ("_" );
95
+ for (String part : last ) {
96
+ if (part .isEmpty ()) break ;
97
+
98
+ nameBuilder
99
+ .append (Character .toUpperCase (part .charAt (0 )))
100
+ .append (part , 1 , part .length ());
101
+ }
102
+ nameBuilder .append ("Service" );
103
+
104
+ return nameBuilder .toString ();
105
+ }
106
+
107
+ /**
108
+ * Build the full class name for a service by joining the specified package elements
109
+ * with '.' and appending the given simple class name.
110
+ *
111
+ * @param elts the array from which to retrieve the package elements
112
+ * @param firstElement the first element to use
113
+ * @param end the end index at which to stop building the package name
114
+ * @param simpleName the simple class name for the service
115
+ * @return the full class name for the service
116
+ */
117
+ private static String buildClassName (String [] elts , int firstElement , int end , String simpleName ) {
118
+ StringBuilder nameBuilder = new StringBuilder ();
119
+ for (int offset = firstElement ; offset < end ; offset ++) {
120
+ // avoid blank elements from leading or double slashes
121
+ if (elts [offset ].isEmpty ()) continue ;
122
+
123
+ nameBuilder
124
+ .append (elts [offset ].toLowerCase ())
125
+ .append ('.' );
60
126
}
61
- finName .append ("Service" );
62
127
63
- // We don't want a package name beginning with dots, so we remove them
64
- String className = finName .toString ().replaceAll ("^\\ .*" ,"" );
128
+ nameBuilder .append (simpleName );
65
129
66
- // quietly try to load the class
67
- Class theClass = runtime .getJavaSupport ().loadJavaClass (className );
68
- return new ClassExtensionLibrary (className + ".java" , theClass );
69
- } catch (ClassNotFoundException cnfe ) {
70
- if (runtime .isDebug ()) cnfe .printStackTrace ();
130
+ return nameBuilder .toString ();
131
+ }
132
+
133
+ /**
134
+ * Try loading the given service class. Rather than raise ClassNotFoundException for
135
+ * jars that do not contain any service class, we require that the service class be a
136
+ * "normal" .class file accessible as a classloader resource. If it can be found
137
+ * using ClassLoader.getResource, we proceed to attempt to load it as a class.
138
+ *
139
+ * @param runtime the Ruby runtime into which the extension service will load
140
+ * @param className the class name of the service class
141
+ * @return a ClassExtensionLibrary if the service class was found; null otherwise.
142
+ */
143
+ private static ClassExtensionLibrary tryServiceLoad (Ruby runtime , String className ) {
144
+ String classFile = className .replaceAll ("\\ ." , "/" ) + ".class" ;
145
+
146
+ try {
147
+ // quietly try to load the class, which must be reachable as a .class resource
148
+ URL resource = runtime .getJRubyClassLoader ().getResource (classFile );
149
+ if (resource != null ) {
150
+ Class theClass = runtime .getJavaSupport ().loadJavaClass (className );
151
+ return new ClassExtensionLibrary (className + ".java" , theClass );
152
+ }
153
+ } catch (ClassNotFoundException cnfe ) {
154
+ if (runtime .isDebug ()) cnfe .printStackTrace ();
155
+ } catch (UnsupportedClassVersionError ucve ) {
156
+ if (runtime .isDebug ()) ucve .printStackTrace ();
157
+ throw runtime .newLoadError ("JRuby ext built for wrong Java version in `" + className + "': " + ucve , className );
158
+ }
71
159
72
- // So apparently the class doesn't exist
160
+ // The class doesn't exist
73
161
return null ;
74
- } catch (UnsupportedClassVersionError ucve ) {
75
- if (runtime .isDebug ()) ucve .printStackTrace ();
76
- throw runtime .newLoadError ("JRuby ext built for wrong Java version in `" + finName + "': " + ucve , finName .toString ());
77
- }
78
162
}
79
163
80
164
public ClassExtensionLibrary (String name , Class extension ) {
0 commit comments