1 /* 2 * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /** 25 * @test 26 * @bug 8072480 8203814 27 * @summary Check the platform classpath contains the correct elements. 28 * @library /tools/lib 29 * @modules jdk.compiler/com.sun.tools.javac.code 30 * jdk.compiler/com.sun.tools.javac.api 31 * jdk.compiler/com.sun.tools.javac.main 32 * jdk.compiler/com.sun.tools.javac.platform 33 * jdk.compiler/com.sun.tools.javac.util 34 * jdk.jdeps/com.sun.tools.classfile 35 * jdk.jdeps/com.sun.tools.javap 36 * @build toolbox.ToolBox ElementStructureTest 37 * @run main ElementStructureTest 38 */ 39 40 import java.io.BufferedReader; 41 import java.io.ByteArrayInputStream; 42 import java.io.ByteArrayOutputStream; 43 import java.io.File; 44 import java.io.IOException; 45 import java.io.InputStream; 46 import java.io.OutputStream; 47 import java.io.OutputStreamWriter; 48 import java.io.Reader; 49 import java.io.Writer; 50 import java.net.URI; 51 import java.net.URL; 52 import java.net.URLClassLoader; 53 import java.nio.file.Files; 54 import java.nio.file.Path; 55 import java.nio.file.Paths; 56 import java.security.MessageDigest; 57 import java.util.ArrayList; 58 import java.util.Arrays; 59 import java.util.Collections; 60 import java.util.EnumSet; 61 import java.util.HashMap; 62 import java.util.Iterator; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Map.Entry; 66 import java.util.ServiceLoader; 67 import java.util.Set; 68 import java.util.TreeMap; 69 import java.util.TreeSet; 70 import java.util.function.Predicate; 71 import java.util.regex.Pattern; 72 import java.util.stream.Stream; 73 74 import javax.lang.model.element.AnnotationMirror; 75 import javax.lang.model.element.AnnotationValue; 76 import javax.lang.model.element.Element; 77 import javax.lang.model.element.ElementVisitor; 78 import javax.lang.model.element.ExecutableElement; 79 import javax.lang.model.element.Modifier; 80 import javax.lang.model.element.ModuleElement; 81 import javax.lang.model.element.NestingKind; 82 import javax.lang.model.element.PackageElement; 83 import javax.lang.model.element.TypeElement; 84 import javax.lang.model.element.TypeParameterElement; 85 import javax.lang.model.element.VariableElement; 86 import javax.lang.model.type.TypeMirror; 87 import javax.tools.FileObject; 88 import javax.tools.JavaCompiler; 89 import javax.tools.JavaFileManager; 90 import javax.tools.JavaFileObject; 91 import javax.tools.JavaFileObject.Kind; 92 import javax.tools.StandardLocation; 93 import javax.tools.ToolProvider; 94 95 import com.sun.source.util.JavacTask; 96 import com.sun.tools.classfile.ClassFile; 97 import com.sun.tools.classfile.ConstantPoolException; 98 import com.sun.tools.javac.api.JavacTaskImpl; 99 import com.sun.tools.javac.code.Symbol.CompletionFailure; 100 import com.sun.tools.javac.platform.PlatformProvider; 101 102 import toolbox.ToolBox; 103 104 105 /**To generate the hash values for version N, invoke this class like: 106 * 107 * java ElementStructureTest generate-hashes $LANGTOOLS_DIR/make/data/symbols/include.list (<classes-for-N> N)+ 108 * 109 * Where <classes-for-N> is the file produced by make/src/classes/build/tools/symbolgenerator/Probe.java. 110 * So, to produce hashes for 6, 7 and 8, this command can be used: 111 * 112 * java ElementStructureTest generate-hashes classes-6 6 classes-7 7 classes-8 8 113 * 114 * To inspect differences between the actual and expected output for version N, invoke this class like: 115 * 116 * java ElementStructureTest generate-output $LANGTOOLS_DIR/make/data/symbols/include.list (<classes-for-N> N <actual-output-file> <expected-output-file>)+ 117 * 118 * For example, to get the actual and expected output for 6 in /tmp/actual and /tmp/expected, respectively: 119 * 120 * java ElementStructureTest generate-output $LANGTOOLS_DIR/make/data/symbols/include.list classes-6 6 /tmp/actual /tmp/expected 121 */ 122 public class ElementStructureTest { 123 124 static final byte[] hash6 = new byte[] { 125 (byte) 0x99, (byte) 0x34, (byte) 0x82, (byte) 0xCF, 126 (byte) 0xE0, (byte) 0x53, (byte) 0xF3, (byte) 0x13, 127 (byte) 0x4E, (byte) 0xCF, (byte) 0x49, (byte) 0x32, 128 (byte) 0xB7, (byte) 0x52, (byte) 0x0F, (byte) 0x68 129 }; 130 static final byte[] hash7 = new byte[] { 131 (byte) 0x3C, (byte) 0x03, (byte) 0xEA, (byte) 0x4A, 132 (byte) 0x62, (byte) 0xD2, (byte) 0x18, (byte) 0xE5, 133 (byte) 0xA5, (byte) 0xC2, (byte) 0xB7, (byte) 0x85, 134 (byte) 0x90, (byte) 0xFA, (byte) 0x98, (byte) 0xCD 135 }; 136 static final byte[] hash8 = new byte[] { 137 (byte) 0x0B, (byte) 0xEB, (byte) 0x16, (byte) 0xF5, 138 (byte) 0x7F, (byte) 0xB0, (byte) 0x18, (byte) 0xF1, 139 (byte) 0x78, (byte) 0x11, (byte) 0xED, (byte) 0x30, 140 (byte) 0x19, (byte) 0x4D, (byte) 0xDE, (byte) 0x8A 141 }; 142 143 final static Map<String, byte[]> version2Hash = new HashMap<>(); 144 145 static { 146 version2Hash.put("6", hash6); 147 version2Hash.put("7", hash7); 148 version2Hash.put("8", hash8); 149 } 150 151 public static void main(String... args) throws Exception { 152 if (args.length == 0) { 153 new ElementStructureTest().doTest(); 154 return ; 155 } 156 switch (args[0]) { 157 case "generate-hashes": 158 new ElementStructureTest().generateHashes(args); 159 break; 160 case "generate-output": 161 new ElementStructureTest().generateOutput(args); 162 break; 163 default: 164 throw new IllegalStateException("Unrecognized request: " + args[0]); 165 } 166 } 167 168 void doTest() throws Exception { 169 for (PlatformProvider provider : ServiceLoader.load(PlatformProvider.class)) { 170 for (String ver : provider.getSupportedPlatformNames()) { 171 if (!version2Hash.containsKey(ver)) 172 continue; 173 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); Writer output = new OutputStreamWriter(baos, "UTF-8")) { 174 run(output, ver); 175 output.close(); 176 byte[] actual = MessageDigest.getInstance("MD5").digest(baos.toByteArray()); 177 if (!Arrays.equals(version2Hash.get(ver), actual)) 178 throw new AssertionError("Wrong hash: " + toHex(actual) + " for version: " + ver); 179 } 180 } 181 } 182 } 183 184 void generateHashes(String... args) throws Exception { 185 Predicate<String> ignoreList = constructAcceptIgnoreList(args[1]); 186 for (int i = 2; i < args.length; i += 2) { 187 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); Writer output = new OutputStreamWriter(baos, "UTF-8")) { 188 realClasses(args[i], ignoreList, output, args[i + 1]); 189 output.close(); 190 System.err.println("version:" + args[i + 1] + "; " + toHex(MessageDigest.getInstance("MD5").digest(baos.toByteArray()))); 191 } 192 } 193 } 194 195 void generateOutput(String... args) throws Exception { 196 Predicate<String> ignoreList = constructAcceptIgnoreList(args[1]); 197 for (int i = 2; i < args.length; i += 4) { 198 try (Writer actual = Files.newBufferedWriter(Paths.get(args[i + 2])); 199 Writer expected = Files.newBufferedWriter(Paths.get(args[i + 3]))) { 200 run(actual, args[i + 1]); 201 realClasses(args[i], ignoreList, expected, args[i + 1]); 202 } 203 } 204 } 205 206 Predicate<String> constructAcceptIgnoreList(String fromFiles) throws IOException { 207 StringBuilder acceptPattern = new StringBuilder(); 208 StringBuilder rejectPattern = new StringBuilder(); 209 for (String file : fromFiles.split(File.pathSeparator)) { 210 try (Stream<String> lines = Files.lines(Paths.get(file))) { 211 lines.forEach(line -> { 212 if (line.isEmpty()) 213 return; 214 StringBuilder targetPattern; 215 switch (line.charAt(0)) { 216 case '+': 217 targetPattern = acceptPattern; 218 break; 219 case '-': 220 targetPattern = rejectPattern; 221 break; 222 default: 223 return ; 224 } 225 line = line.substring(1); 226 if (line.endsWith("/")) { 227 line += "[^/]*"; 228 } else { 229 line += "|" + line + "$[^/]*"; 230 } 231 line = line.replace("/", "."); 232 if (targetPattern.length() != 0) 233 targetPattern.append("|"); 234 targetPattern.append(line); 235 }); 236 } 237 } 238 Pattern accept = Pattern.compile(acceptPattern.toString()); 239 Pattern reject = Pattern.compile(rejectPattern.toString()); 240 241 return clazzName -> accept.matcher(clazzName).matches() && !reject.matcher(clazzName).matches(); 242 } 243 244 private static String toHex(byte[] bytes) { 245 StringBuilder hex = new StringBuilder(); 246 String delim = ""; 247 248 for (byte b : bytes) { 249 hex.append(delim); 250 hex.append(String.format("(byte) 0x%02X", b)); 251 delim = ", "; 252 } 253 254 return hex.toString(); 255 } 256 257 void run(Writer output, String version) throws Exception { 258 List<String> options = Arrays.asList("--release", version, "-classpath", ""); 259 List<ToolBox.JavaSource> files = Arrays.asList(new ToolBox.JavaSource("Test", "")); 260 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 261 JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, null, null, options, null, files); 262 263 task.analyze(); 264 265 JavaFileManager fm = task.getContext().get(JavaFileManager.class); 266 267 for (String pack : packages(fm)) { 268 PackageElement packEl = task.getElements().getPackageElement(pack); 269 if (packEl == null) { 270 throw new AssertionError("Cannot find package: " + pack); 271 } 272 new ExhaustiveElementScanner(task, output, p -> true).visit(packEl); 273 } 274 } 275 276 void realClasses(String location, Predicate<String> acceptor, Writer output, String version) throws Exception { 277 Path classes = Paths.get(location); 278 Map<String, JavaFileObject> className2File = new HashMap<>(); 279 Map<JavaFileObject, String> file2ClassName = new HashMap<>(); 280 281 try (BufferedReader descIn = Files.newBufferedReader(classes)) { 282 String classFileData; 283 284 while ((classFileData = descIn.readLine()) != null) { 285 ByteArrayOutputStream data = new ByteArrayOutputStream(); 286 for (int i = 0; i < classFileData.length(); i += 2) { 287 data.write(Integer.parseInt(classFileData.substring(i, i + 2), 16)); 288 } 289 JavaFileObject file = new ByteArrayJavaFileObject(data.toByteArray()); 290 try (InputStream in = new ByteArrayInputStream(data.toByteArray())) { 291 String name = ClassFile.read(in).getName().replace("/", "."); 292 className2File.put(name, file); 293 file2ClassName.put(file, name); 294 } catch (IOException | ConstantPoolException ex) { 295 throw new IllegalStateException(ex); 296 } 297 } 298 } 299 300 try (JavaFileManager fm = new TestFileManager(className2File, file2ClassName)) { 301 JavacTaskImpl task = (JavacTaskImpl) ToolProvider.getSystemJavaCompiler().getTask(null, fm, null, Arrays.asList("-source", version), null, Arrays.asList(new ToolBox.JavaSource("Test", ""))); 302 task.parse(); 303 304 PACK: for (String pack : packages(fm)) { 305 PackageElement packEl = task.getElements().getPackageElement(pack); 306 assert packEl != null; 307 new ExhaustiveElementScanner(task, output, acceptor).visit(packEl); 308 } 309 } 310 } 311 312 Set<String> packages(JavaFileManager fm) throws IOException { 313 Set<String> packages = new TreeSet<>(); 314 EnumSet<Kind> kinds = EnumSet.of(JavaFileObject.Kind.CLASS, JavaFileObject.Kind.OTHER); 315 316 for (JavaFileObject file : fm.list(StandardLocation.PLATFORM_CLASS_PATH, "", kinds, true)) { 317 String binary = fm.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, file); 318 packages.add(binary.substring(0, binary.lastIndexOf('.'))); 319 } 320 321 return packages; 322 } 323 324 final class ExhaustiveElementScanner implements ElementVisitor<Void, Void> { 325 326 final JavacTask task; 327 final Writer out; 328 final Predicate<String> acceptType; 329 330 public ExhaustiveElementScanner(JavacTask task, Writer out, Predicate<String> acceptType) { 331 this.task = task; 332 this.out = out; 333 this.acceptType = acceptType; 334 } 335 336 @Override 337 public Void visit(Element e, Void p) { 338 return e.accept(this, p); 339 } 340 341 @Override 342 public Void visit(Element e) { 343 return e.accept(this, null); 344 } 345 346 private void write(TypeMirror type) throws IOException { 347 try { 348 out.write(type.toString() 349 .replace("java.lang.invoke.MethodHandle$PolymorphicSignature", "java.lang.invoke.MethodHandle.PolymorphicSignature") 350 .replace("javax.swing.JRootPane$DefaultAction", "javax.swing.JRootPane.DefaultAction") 351 .replace("javax.swing.plaf.metal.MetalFileChooserUI$DirectoryComboBoxRenderer", "javax.swing.plaf.metal.MetalFileChooserUI.DirectoryComboBoxRenderer") 352 ); 353 } catch (CompletionFailure cf) { 354 out.write("cf"); 355 } 356 } 357 358 private void writeTypes(Iterable<? extends TypeMirror> types) throws IOException { 359 String sep = ""; 360 361 for (TypeMirror type : types) { 362 out.write(sep); 363 write(type); 364 sep = ", "; 365 } 366 } 367 368 private void writeAnnotations(Iterable<? extends AnnotationMirror> annotations) throws IOException { 369 for (AnnotationMirror ann : annotations) { 370 out.write("@"); 371 write(ann.getAnnotationType()); 372 if (!ann.getElementValues().isEmpty()) { 373 out.write("("); 374 Map<ExecutableElement, AnnotationValue> valuesMap = new TreeMap<>((a1, a2) -> a1.getSimpleName().toString().compareTo(a2.getSimpleName().toString())); 375 valuesMap.putAll(ann.getElementValues()); 376 for (Entry<? extends ExecutableElement, ? extends AnnotationValue> ev : valuesMap.entrySet()) { 377 out.write(ev.getKey().getSimpleName().toString()); 378 out.write(" = "); 379 out.write(ev.getValue().toString()); 380 } 381 out.write(")"); 382 } 383 } 384 } 385 386 void analyzeElement(Element e) { 387 try { 388 write(e.asType()); 389 writeAnnotations(e.getAnnotationMirrors()); 390 out.write(e.getKind().toString()); 391 out.write(e.getModifiers().toString()); 392 out.write(e.getSimpleName().toString()); 393 } catch (IOException ex) { 394 ex.printStackTrace(); 395 } 396 } 397 398 boolean acceptAccess(Element e) { 399 return e.getModifiers().contains(Modifier.PUBLIC) || e.getModifiers().contains(Modifier.PROTECTED); 400 } 401 402 @Override 403 public Void visitExecutable(ExecutableElement e, Void p) { 404 if (!acceptAccess(e)) 405 return null; 406 try { 407 analyzeElement(e); 408 out.write(String.valueOf(e.getDefaultValue())); 409 for (VariableElement param : e.getParameters()) { 410 visit(param, p); 411 } 412 out.write(String.valueOf(typeMirrorTranslate(e.getReceiverType()))); 413 write(e.getReturnType()); 414 out.write(e.getSimpleName().toString()); 415 writeTypes(e.getThrownTypes()); 416 for (TypeParameterElement param : e.getTypeParameters()) { 417 visit(param, p); 418 } 419 out.write(String.valueOf(e.isDefault())); 420 out.write(String.valueOf(e.isVarArgs())); 421 out.write("\n"); 422 } catch (IOException ex) { 423 ex.printStackTrace(); 424 } 425 return null; 426 } 427 428 /** 429 * Original implementation of getReceiverType returned null 430 * for many cases where TypeKind.NONE was specified; translate 431 * back to null to compare against old hashes. 432 */ 433 private TypeMirror typeMirrorTranslate(TypeMirror type) { 434 if (type.getKind() == javax.lang.model.type.TypeKind.NONE) 435 return null; 436 else 437 return type; 438 } 439 440 @Override 441 public Void visitPackage(PackageElement e, Void p) { 442 List<Element> types = new ArrayList<>(e.getEnclosedElements()); 443 Collections.sort(types, (e1, e2) -> e1.getSimpleName().toString().compareTo(e2.getSimpleName().toString())); 444 for (Element encl : types) { 445 visit(encl, p); 446 } 447 return null; 448 } 449 450 @Override 451 public Void visitType(TypeElement e, Void p) { 452 if (!acceptAccess(e)) 453 return null; 454 writeType(e); 455 return null; 456 } 457 458 void writeType(TypeElement e) { 459 if (!acceptType.test(task.getElements().getBinaryName(e).toString())) 460 return ; 461 try { 462 analyzeElement(e); 463 writeTypes(e.getInterfaces()); 464 out.write(e.getNestingKind().toString()); 465 out.write(e.getQualifiedName().toString()); 466 write(e.getSuperclass()); 467 for (TypeParameterElement param : e.getTypeParameters()) { 468 visit(param, null); 469 } 470 List<Element> defs = new ArrayList<>(e.getEnclosedElements()); //XXX: forcing ordering for members - not completely correct! 471 Collections.sort(defs, (e1, e2) -> e1.toString().compareTo(e2.toString())); 472 for (Element def : defs) { 473 visit(def, null); 474 } 475 out.write("\n"); 476 } catch (IOException ex) { 477 ex.printStackTrace(); 478 } 479 } 480 481 @Override 482 public Void visitVariable(VariableElement e, Void p) { 483 if (!acceptAccess(e)) 484 return null; 485 try { 486 analyzeElement(e); 487 out.write(String.valueOf(e.getConstantValue())); 488 out.write("\n"); 489 } catch (IOException ex) { 490 ex.printStackTrace(); 491 } 492 return null; 493 } 494 495 @Override 496 public Void visitTypeParameter(TypeParameterElement e, Void p) { 497 try { 498 analyzeElement(e); 499 out.write(e.getBounds().toString()); 500 out.write("\n"); 501 } catch (IOException ex) { 502 ex.printStackTrace(); 503 } 504 return null; 505 } 506 507 @Override 508 public Void visitModule(ModuleElement e, Void p) { 509 throw new IllegalStateException("Not supported yet."); 510 } 511 512 @Override 513 public Void visitUnknown(Element e, Void p) { 514 throw new IllegalStateException("Should not get here."); 515 } 516 517 } 518 519 final class TestFileManager implements JavaFileManager { 520 521 final Map<String, JavaFileObject> className2File; 522 final Map<JavaFileObject, String> file2ClassName; 523 524 public TestFileManager(Map<String, JavaFileObject> className2File, Map<JavaFileObject, String> file2ClassName) { 525 this.className2File = className2File; 526 this.file2ClassName = file2ClassName; 527 } 528 529 @Override 530 public ClassLoader getClassLoader(Location location) { 531 return new URLClassLoader(new URL[0]); 532 } 533 534 @Override 535 public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException { 536 if (location != StandardLocation.PLATFORM_CLASS_PATH || !kinds.contains(Kind.CLASS)) 537 return Collections.emptyList(); 538 539 if (!packageName.isEmpty()) 540 packageName += "."; 541 542 List<JavaFileObject> result = new ArrayList<>(); 543 544 for (Entry<String, JavaFileObject> e : className2File.entrySet()) { 545 String currentPackage = e.getKey().substring(0, e.getKey().lastIndexOf(".") + 1); 546 if (recurse ? currentPackage.startsWith(packageName) : packageName.equals(currentPackage)) 547 result.add(e.getValue()); 548 } 549 550 return result; 551 } 552 553 @Override 554 public String inferBinaryName(Location location, JavaFileObject file) { 555 return file2ClassName.get(file); 556 } 557 558 @Override 559 public boolean isSameFile(FileObject a, FileObject b) { 560 return a == b; 561 } 562 563 @Override 564 public boolean handleOption(String current, Iterator<String> remaining) { 565 return false; 566 } 567 568 @Override 569 public boolean hasLocation(Location location) { 570 return location == StandardLocation.PLATFORM_CLASS_PATH; 571 } 572 573 @Override 574 public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException { 575 if (location != StandardLocation.PLATFORM_CLASS_PATH || kind != Kind.CLASS) 576 return null; 577 578 return className2File.get(className); 579 } 580 581 @Override 582 public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException { 583 throw new UnsupportedOperationException(""); 584 } 585 586 @Override 587 public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { 588 return null; 589 } 590 591 @Override 592 public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException { 593 throw new UnsupportedOperationException(""); 594 } 595 596 @Override 597 public void flush() throws IOException { 598 } 599 600 @Override 601 public void close() throws IOException { 602 } 603 604 @Override 605 public int isSupportedOption(String option) { 606 return -1; 607 } 608 609 } 610 611 static class ByteArrayJavaFileObject implements JavaFileObject { 612 613 private final byte[] data; 614 615 public ByteArrayJavaFileObject(byte[] data) { 616 this.data = data; 617 } 618 619 @Override 620 public Kind getKind() { 621 return Kind.CLASS; 622 } 623 624 @Override 625 public boolean isNameCompatible(String simpleName, Kind kind) { 626 return true; 627 } 628 629 @Override 630 public NestingKind getNestingKind() { 631 return null; 632 } 633 634 @Override 635 public Modifier getAccessLevel() { 636 return null; 637 } 638 639 @Override 640 public URI toUri() { 641 return null; 642 } 643 644 @Override 645 public String getName() { 646 return null; 647 } 648 649 @Override 650 public InputStream openInputStream() throws IOException { 651 return new ByteArrayInputStream(data); 652 } 653 654 @Override 655 public OutputStream openOutputStream() throws IOException { 656 throw new UnsupportedOperationException(); 657 } 658 659 @Override 660 public Reader openReader(boolean ignoreEncodingErrors) throws IOException { 661 throw new UnsupportedOperationException(); 662 } 663 664 @Override 665 public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { 666 throw new UnsupportedOperationException(); 667 } 668 669 @Override 670 public Writer openWriter() throws IOException { 671 throw new UnsupportedOperationException(); 672 } 673 674 @Override 675 public long getLastModified() { 676 return 0; 677 } 678 679 @Override 680 public boolean delete() { 681 throw new UnsupportedOperationException(); 682 } 683 } 684 685 }