12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package jdk.tools.jlink.internal.plugins; 26 27 import java.io.BufferedReader; 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.lang.invoke.MethodType; 33 import java.nio.file.Files; 34 import java.util.EnumSet; 35 import java.util.Map; 36 import java.util.Set; 37 import java.util.TreeMap; 38 import java.util.TreeSet; 39 import java.util.stream.Stream; 40 41 import jdk.internal.access.JavaLangInvokeAccess; 42 import jdk.internal.access.SharedSecrets; 43 import jdk.tools.jlink.plugin.Plugin; 44 import jdk.tools.jlink.plugin.PluginException; 45 import jdk.tools.jlink.plugin.ResourcePool; 46 import jdk.tools.jlink.plugin.ResourcePoolBuilder; 47 import jdk.tools.jlink.plugin.ResourcePoolEntry; 48 49 /** 50 * Plugin to generate java.lang.invoke classes. 51 * 52 * The plugin reads in a file generated by running any application with 53 * {@code -Djava.lang.invoke.MethodHandle.TRACE_RESOLVE=true}. This is done 54 * automatically during build, see make/GenerateLinkOptData.gmk. See 55 * build/tools/classlist/HelloClasslist.java for the training application. 56 * 57 * HelloClasslist tries to reflect common use of java.lang.invoke during early 58 * startup and warmup in various applications. To ensure a good default 59 * trade-off between static footprint and startup the application should be 60 * relatively conservative. 61 * 62 * When using jlink to build a custom application runtime, generating a trace 63 * file using {@code -Djava.lang.invoke.MethodHandle.TRACE_RESOLVE=true} and 64 * feeding that into jlink using {@code --generate-jli-classes=@trace_file} can 65 * help improve startup time. 66 */ 67 public final class GenerateJLIClassesPlugin implements Plugin { 68 69 private static final String NAME = "generate-jli-classes"; 70 71 private static final String DESCRIPTION = PluginsResourceBundle.getDescription(NAME); 72 73 private static final String DEFAULT_TRACE_FILE = "default_jli_trace.txt"; 74 75 private static final String DIRECT_HOLDER = "java/lang/invoke/DirectMethodHandle$Holder"; 76 private static final String DMH_INVOKE_VIRTUAL = "invokeVirtual"; 77 private static final String DMH_INVOKE_STATIC = "invokeStatic"; 78 private static final String DMH_INVOKE_SPECIAL = "invokeSpecial"; 79 private static final String DMH_NEW_INVOKE_SPECIAL = "newInvokeSpecial"; 80 private static final String DMH_INVOKE_INTERFACE = "invokeInterface"; 81 private static final String DMH_INVOKE_STATIC_INIT = "invokeStaticInit"; 82 private static final String DMH_INVOKE_SPECIAL_IFC = "invokeSpecialIFC"; 83 84 private static final String DELEGATING_HOLDER = "java/lang/invoke/DelegatingMethodHandle$Holder"; 85 private static final String BASIC_FORMS_HOLDER = "java/lang/invoke/LambdaForm$Holder"; 86 87 private static final String INVOKERS_HOLDER_NAME = "java.lang.invoke.Invokers$Holder"; 88 private static final String INVOKERS_HOLDER_INTERNAL_NAME = INVOKERS_HOLDER_NAME.replace('.', '/'); 89 90 private static final JavaLangInvokeAccess JLIA 91 = SharedSecrets.getJavaLangInvokeAccess(); 92 93 private final TreeSet<String> speciesTypes = new TreeSet<>(); 94 95 private final TreeSet<String> invokerTypes = new TreeSet<>(); 96 97 private final TreeSet<String> callSiteTypes = new TreeSet<>(); 98 99 private final Map<String, Set<String>> dmhMethods = new TreeMap<>(); 100 101 String mainArgument; 102 103 public GenerateJLIClassesPlugin() { 104 } 105 106 @Override 107 public String getName() { 108 return NAME; 109 } 110 111 @Override 112 public String getDescription() { 113 return DESCRIPTION; 114 } 115 116 @Override 117 public Set<State> getState() { 118 return EnumSet.of(State.AUTO_ENABLED, State.FUNCTIONAL); 119 } 120 121 @Override 122 public boolean hasArguments() { 123 return true; 124 } 125 126 @Override 127 public String getArgumentsDescription() { 128 return PluginsResourceBundle.getArgument(NAME); 129 } 130 131 private static int DMH_INVOKE_VIRTUAL_TYPE = 0; 132 private static int DMH_INVOKE_INTERFACE_TYPE = 4; 133 134 // Map from DirectMethodHandle method type to internal ID, matching values 135 // of the corresponding constants in java.lang.invoke.MethodTypeForm 136 private static final Map<String, Integer> DMH_METHOD_TYPE_MAP = 137 Map.of( 138 DMH_INVOKE_VIRTUAL, DMH_INVOKE_VIRTUAL_TYPE, 139 DMH_INVOKE_STATIC, 1, 140 DMH_INVOKE_SPECIAL, 2, 141 DMH_NEW_INVOKE_SPECIAL, 3, 142 DMH_INVOKE_INTERFACE, DMH_INVOKE_INTERFACE_TYPE, 143 DMH_INVOKE_STATIC_INIT, 5, 144 DMH_INVOKE_SPECIAL_IFC, 20 145 ); 146 147 @Override 148 public void configure(Map<String, String> config) { 149 mainArgument = config.get(NAME); 150 } 151 152 private void addSpeciesType(String type) { 153 speciesTypes.add(expandSignature(type)); 154 } 155 156 private void addInvokerType(String methodType) { 157 validateMethodType(methodType); 158 invokerTypes.add(methodType); 159 } 160 161 private void addCallSiteType(String csType) { 162 validateMethodType(csType); 163 callSiteTypes.add(csType); 164 } 165 166 public void initialize(ResourcePool in) { 167 // Load configuration from the contents in the supplied input file 168 // - if none was supplied we look for the default file 169 if (mainArgument == null || !mainArgument.startsWith("@")) { 170 try (InputStream traceFile = 171 this.getClass().getResourceAsStream(DEFAULT_TRACE_FILE)) { 172 if (traceFile != null) { 173 readTraceConfig( 174 new BufferedReader( 175 new InputStreamReader(traceFile)).lines()); 176 } 177 } catch (Exception e) { 178 throw new PluginException("Couldn't read " + DEFAULT_TRACE_FILE, e); 179 } 180 } else { 181 File file = new File(mainArgument.substring(1)); 182 if (file.exists()) { 183 readTraceConfig(fileLines(file)); 184 } 185 } 186 } 187 188 private void readTraceConfig(Stream<String> lines) { 189 lines.map(line -> line.split(" ")) 190 .forEach(parts -> { 191 switch (parts[0]) { 192 case "[SPECIES_RESOLVE]": 193 // Allow for new types of species data classes being resolved here 194 if (parts.length == 3 && parts[1].startsWith("java.lang.invoke.BoundMethodHandle$Species_")) { 195 String species = parts[1].substring("java.lang.invoke.BoundMethodHandle$Species_".length()); 196 if (!"L".equals(species)) { 197 addSpeciesType(species); 198 } 199 } 200 break; 201 case "[LF_RESOLVE]": 202 String methodType = parts[3]; 203 if (parts[1].equals(INVOKERS_HOLDER_NAME)) { 204 if ("linkToTargetMethod".equals(parts[2]) || 205 "linkToCallSite".equals(parts[2])) { 206 addCallSiteType(methodType); 207 } else { 208 addInvokerType(methodType); 209 } 210 } else if (parts[1].contains("DirectMethodHandle")) { 211 String dmh = parts[2]; 212 // ignore getObject etc for now (generated 213 // by default) 214 if (DMH_METHOD_TYPE_MAP.containsKey(dmh)) { 215 addDMHMethodType(dmh, methodType); 216 } 217 } 218 break; 219 default: break; // ignore 220 } 221 }); 222 } 223 224 private void addDMHMethodType(String dmh, String methodType) { 225 validateMethodType(methodType); 226 Set<String> methodTypes = dmhMethods.get(dmh); 227 if (methodTypes == null) { 228 methodTypes = new TreeSet<>(); 229 dmhMethods.put(dmh, methodTypes); 230 } 231 methodTypes.add(methodType); 232 } 233 234 private Stream<String> fileLines(File file) { 235 try { 236 return Files.lines(file.toPath()); 237 } catch (IOException io) { 238 throw new PluginException("Couldn't read file"); 239 } 240 } 241 242 private void validateMethodType(String type) { 243 String[] typeParts = type.split("_"); 244 // check return type (second part) 245 if (typeParts.length != 2 || typeParts[1].length() != 1 246 || "LJIFDV".indexOf(typeParts[1].charAt(0)) == -1) { 247 throw new PluginException( 248 "Method type signature must be of form [LJIFD]*_[LJIFDV]"); 249 } 250 // expand and check arguments (first part) 251 expandSignature(typeParts[0]); 252 } 253 254 private static void requireBasicType(char c) { 255 if ("LIJFD".indexOf(c) < 0) { 256 throw new PluginException( 257 "Character " + c + " must correspond to a basic field type: LIJFD"); 258 } 259 } 260 261 @Override 262 public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { 263 initialize(in); 264 // Copy all but DMH_ENTRY to out 265 in.transformAndCopy(entry -> { 266 // filter out placeholder entries 267 String path = entry.path(); 268 if (path.equals(DIRECT_METHOD_HOLDER_ENTRY) || 269 path.equals(DELEGATING_METHOD_HOLDER_ENTRY) || 270 path.equals(INVOKERS_HOLDER_ENTRY) || 271 path.equals(BASIC_FORMS_HOLDER_ENTRY)) { 272 return null; 273 } else { 274 return entry; 275 } 276 }, out); 277 278 // Generate BMH Species classes 279 speciesTypes.forEach(types -> generateBMHClass(types, out)); 280 281 // Generate LambdaForm Holder classes 282 generateHolderClasses(out); 283 284 // Let it go 285 speciesTypes.clear(); 286 invokerTypes.clear(); 287 callSiteTypes.clear(); 288 dmhMethods.clear(); 289 290 return out.build(); 291 } 292 293 private void generateBMHClass(String types, ResourcePoolBuilder out) { 294 try { 295 // Generate class 296 Map.Entry<String, byte[]> result = 297 JLIA.generateConcreteBMHClassBytes(types); 298 String className = result.getKey(); 299 byte[] bytes = result.getValue(); 300 301 // Add class to pool 302 ResourcePoolEntry ndata = ResourcePoolEntry.create( 303 "/java.base/" + className + ".class", 304 bytes); 305 out.add(ndata); 306 } catch (Exception ex) { 307 throw new PluginException(ex); 308 } 309 } 310 311 private void generateHolderClasses(ResourcePoolBuilder out) { 312 int count = 0; 313 for (Set<String> entry : dmhMethods.values()) { 314 count += entry.size(); 315 } 316 MethodType[] directMethodTypes = new MethodType[count]; 317 int[] dmhTypes = new int[count]; 318 int index = 0; 319 for (Map.Entry<String, Set<String>> entry : dmhMethods.entrySet()) { 320 String dmhType = entry.getKey(); 321 for (String type : entry.getValue()) { 322 // The DMH type to actually ask for is retrieved by removing 323 // the first argument, which needs to be of Object.class 324 MethodType mt = asMethodType(type); 325 if (mt.parameterCount() < 1 || 326 mt.parameterType(0) != Object.class) { 327 throw new PluginException( 328 "DMH type parameter must start with L: " + dmhType + " " + type); 329 } 330 331 // Adapt the method type of the LF to retrieve 332 directMethodTypes[index] = mt.dropParameterTypes(0, 1); 333 334 // invokeVirtual and invokeInterface must have a leading Object 335 // parameter, i.e., the receiver 336 dmhTypes[index] = DMH_METHOD_TYPE_MAP.get(dmhType); 337 if (dmhTypes[index] == DMH_INVOKE_INTERFACE_TYPE || 338 dmhTypes[index] == DMH_INVOKE_VIRTUAL_TYPE) { 339 if (mt.parameterCount() < 2 || 340 mt.parameterType(1) != Object.class) { 341 throw new PluginException( 342 "DMH type parameter must start with LL: " + dmhType + " " + type); 343 } 344 } 345 index++; 346 } 347 } 348 349 // The invoker type to ask for is retrieved by removing the first 350 // and the last argument, which needs to be of Object.class 351 MethodType[] invokerMethodTypes = new MethodType[this.invokerTypes.size()]; 352 int i = 0; 353 for (String invokerType : invokerTypes) { 354 MethodType mt = asMethodType(invokerType); 355 final int lastParam = mt.parameterCount() - 1; 356 if (mt.parameterCount() < 2 || 357 mt.parameterType(0) != Object.class || 358 mt.parameterType(lastParam) != Object.class) { 359 throw new PluginException( 360 "Invoker type parameter must start and end with Object: " + invokerType); 361 } 362 mt = mt.dropParameterTypes(lastParam, lastParam + 1); 363 invokerMethodTypes[i] = mt.dropParameterTypes(0, 1); 364 i++; 365 } 366 367 // The callSite type to ask for is retrieved by removing the last 368 // argument, which needs to be of Object.class 369 MethodType[] callSiteMethodTypes = new MethodType[this.callSiteTypes.size()]; 370 i = 0; 371 for (String callSiteType : callSiteTypes) { 372 MethodType mt = asMethodType(callSiteType); 373 final int lastParam = mt.parameterCount() - 1; 374 if (mt.parameterCount() < 1 || 375 mt.parameterType(lastParam) != Object.class) { 376 throw new PluginException( 377 "CallSite type parameter must end with Object: " + callSiteType); 378 } 379 callSiteMethodTypes[i] = mt.dropParameterTypes(lastParam, lastParam + 1); 380 i++; 381 } 382 try { 383 byte[] bytes = JLIA.generateDirectMethodHandleHolderClassBytes( 384 DIRECT_HOLDER, directMethodTypes, dmhTypes); 385 ResourcePoolEntry ndata = ResourcePoolEntry 386 .create(DIRECT_METHOD_HOLDER_ENTRY, bytes); 387 out.add(ndata); 388 389 bytes = JLIA.generateDelegatingMethodHandleHolderClassBytes( 390 DELEGATING_HOLDER, directMethodTypes); 391 ndata = ResourcePoolEntry.create(DELEGATING_METHOD_HOLDER_ENTRY, bytes); 392 out.add(ndata); 393 394 bytes = JLIA.generateInvokersHolderClassBytes(INVOKERS_HOLDER_INTERNAL_NAME, 395 invokerMethodTypes, callSiteMethodTypes); 396 ndata = ResourcePoolEntry.create(INVOKERS_HOLDER_ENTRY, bytes); 397 out.add(ndata); 398 399 bytes = JLIA.generateBasicFormsClassBytes(BASIC_FORMS_HOLDER); 400 ndata = ResourcePoolEntry.create(BASIC_FORMS_HOLDER_ENTRY, bytes); 401 out.add(ndata); 402 } catch (Exception ex) { 403 throw new PluginException(ex); 404 } 405 } 406 private static final String DIRECT_METHOD_HOLDER_ENTRY = 407 "/java.base/" + DIRECT_HOLDER + ".class"; 408 private static final String DELEGATING_METHOD_HOLDER_ENTRY = 409 "/java.base/" + DELEGATING_HOLDER + ".class"; 410 private static final String BASIC_FORMS_HOLDER_ENTRY = 411 "/java.base/" + BASIC_FORMS_HOLDER + ".class"; 412 private static final String INVOKERS_HOLDER_ENTRY = 413 "/java.base/" + INVOKERS_HOLDER_INTERNAL_NAME + ".class"; 414 415 // Convert LL -> LL, L3 -> LLL 416 public static String expandSignature(String signature) { 417 StringBuilder sb = new StringBuilder(); 418 char last = 'X'; 419 int count = 0; 420 for (int i = 0; i < signature.length(); i++) { 421 char c = signature.charAt(i); 422 if (c >= '0' && c <= '9') { 423 count *= 10; 424 count += (c - '0'); 425 } else { 426 requireBasicType(c); 427 for (int j = 1; j < count; j++) { 428 sb.append(last); 429 } 430 sb.append(c); 431 last = c; 432 count = 0; 433 } 434 } 435 436 // ended with a number, e.g., "L2": append last char count - 1 times 437 if (count > 1) { 438 requireBasicType(last); 439 for (int j = 1; j < count; j++) { 440 sb.append(last); 441 } 442 } 443 return sb.toString(); 444 } 445 446 private static MethodType asMethodType(String basicSignatureString) { 447 String[] parts = basicSignatureString.split("_"); 448 assert(parts.length == 2); 449 assert(parts[1].length() == 1); 450 String parameters = expandSignature(parts[0]); 451 Class<?> rtype = simpleType(parts[1].charAt(0)); 452 if (parameters.isEmpty()) { 453 return MethodType.methodType(rtype); 454 } else { 455 Class<?>[] ptypes = new Class<?>[parameters.length()]; 456 for (int i = 0; i < ptypes.length; i++) { 457 ptypes[i] = simpleType(parameters.charAt(i)); 458 } 459 return MethodType.methodType(rtype, ptypes); 460 } 461 } 462 463 private static Class<?> simpleType(char c) { 464 switch (c) { 465 case 'F': 466 return float.class; 467 case 'D': 468 return double.class; 469 case 'I': 470 return int.class; 471 case 'L': 472 return Object.class; 473 case 'J': 474 return long.class; 475 case 'V': 476 return void.class; 477 case 'Z': 478 case 'B': 479 case 'S': 480 case 'C': 481 throw new IllegalArgumentException("Not a valid primitive: " + c + 482 " (use I instead)"); 483 default: 484 throw new IllegalArgumentException("Not a primitive: " + c); 485 } 486 } 487 } | 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package jdk.tools.jlink.internal.plugins; 26 27 import java.io.BufferedReader; 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.nio.file.Files; 33 import java.util.EnumSet; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 38 import jdk.internal.access.JavaLangInvokeAccess; 39 import jdk.internal.access.SharedSecrets; 40 import jdk.tools.jlink.plugin.Plugin; 41 import jdk.tools.jlink.plugin.PluginException; 42 import jdk.tools.jlink.plugin.ResourcePool; 43 import jdk.tools.jlink.plugin.ResourcePoolBuilder; 44 import jdk.tools.jlink.plugin.ResourcePoolEntry; 45 46 /** 47 * Plugin to generate java.lang.invoke classes. 48 * 49 * The plugin reads in a file generated by running any application with 50 * {@code -Djava.lang.invoke.MethodHandle.TRACE_RESOLVE=true}. This is done 51 * automatically during build, see make/GenerateLinkOptData.gmk. See 52 * build/tools/classlist/HelloClasslist.java for the training application. 53 * 54 * HelloClasslist tries to reflect common use of java.lang.invoke during early 55 * startup and warmup in various applications. To ensure a good default 56 * trade-off between static footprint and startup the application should be 57 * relatively conservative. 58 * 59 * When using jlink to build a custom application runtime, generating a trace 60 * file using {@code -Djava.lang.invoke.MethodHandle.TRACE_RESOLVE=true} and 61 * feeding that into jlink using {@code --generate-jli-classes=@trace_file} can 62 * help improve startup time. 63 */ 64 public final class GenerateJLIClassesPlugin implements Plugin { 65 66 private static final String NAME = "generate-jli-classes"; 67 68 private static final String DESCRIPTION = PluginsResourceBundle.getDescription(NAME); 69 70 private static final String DEFAULT_TRACE_FILE = "default_jli_trace.txt"; 71 72 private static final JavaLangInvokeAccess JLIA = SharedSecrets.getJavaLangInvokeAccess(); 73 74 String mainArgument; 75 String[] lines = null; 76 77 public GenerateJLIClassesPlugin() { 78 } 79 80 @Override 81 public String getName() { 82 return NAME; 83 } 84 85 @Override 86 public String getDescription() { 87 return DESCRIPTION; 88 } 89 90 @Override 91 public Set<State> getState() { 92 return EnumSet.of(State.AUTO_ENABLED, State.FUNCTIONAL); 93 } 94 95 @Override 96 public boolean hasArguments() { 97 return true; 98 } 99 100 @Override 101 public String getArgumentsDescription() { 102 return PluginsResourceBundle.getArgument(NAME); 103 } 104 105 @Override 106 public void configure(Map<String, String> config) { 107 mainArgument = config.get(NAME); 108 } 109 110 // Repeat def, we do not have access right to InvokerBytecodeGeneratorHelper 111 // Must be exact same! 112 private static final String DIRECT_METHOD_HOLDER_ENTRY = 113 "/java.base/java/lang/invoke/DirectMethodHandle$Holder.class"; 114 private static final String DELEGATING_METHOD_HOLDER_ENTRY = 115 "/java.base/java/lang/invoke/DelegatingMethodHandle$Holder.class"; 116 private static final String INVOKERS_HOLDER_ENTRY = 117 "/java.base/java/lang/invoke/Invokers$Holder.class"; 118 private static final String BASIC_FORMS_HOLDER_ENTRY = 119 "/java.base/java/lang/invoke/LambdaForm$Holder.class"; 120 121 @Override 122 public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { 123 initialize(in); 124 // Copy all but DMH_ENTRY to out 125 in.transformAndCopy(entry -> { 126 // filter out placeholder entries 127 String path = entry.path(); 128 if (path.equals(DIRECT_METHOD_HOLDER_ENTRY) || 129 path.equals(DELEGATING_METHOD_HOLDER_ENTRY) || 130 path.equals(INVOKERS_HOLDER_ENTRY) || 131 path.equals(BASIC_FORMS_HOLDER_ENTRY)) { 132 return null; 133 } else { 134 return entry; 135 } 136 }, out); 137 138 // Generate LambdaForm Holder classes 139 generateHolderClasses(out); 140 // clear input. 141 lines = null; 142 return out.build(); 143 } 144 145 private void generateHolderClasses(ResourcePoolBuilder out) { 146 try { 147 Map<String, byte[]> result = JLIA.generateMethodHandleHolderClasses(lines); 148 if (result != null) { 149 result.forEach ((k,v) -> { 150 ResourcePoolEntry ndata = ResourcePoolEntry.create(k, v); 151 out.add(ndata); 152 }); 153 } 154 } catch (Exception ex) { 155 throw new PluginException(ex); 156 } 157 } 158 159 public void initialize(ResourcePool in) { 160 // Load configuration from the contents in the supplied input file 161 // - if none was supplied we look for the default file 162 if (mainArgument == null || !mainArgument.startsWith("@")) { 163 try (InputStream traceFile = 164 this.getClass().getResourceAsStream(DEFAULT_TRACE_FILE)) { 165 if (traceFile != null) { 166 lines = new BufferedReader( 167 new InputStreamReader(traceFile)).lines().toArray(String[]::new); 168 } 169 } catch (Exception e) { 170 throw new PluginException("Couldn't read " + DEFAULT_TRACE_FILE, e); 171 } 172 } else { 173 File file = new File(mainArgument.substring(1)); 174 try { 175 if (file.exists()) { 176 lines = Files.readAllLines(file.toPath()).toArray(new String[0]); 177 } 178 } catch (Exception e) { 179 throw new PluginException("Couldn't read " + file.getName(), e); 180 } 181 } 182 } 183 } |