[Markdown] Clean up strings
[idea/community.git] / platform / util-class-loader / src / com / intellij / util / lang / UrlClassLoader.java
1 // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.util.lang;
3
4 import com.intellij.ReviseWhenPortedToJDK;
5 import com.intellij.openapi.util.text.StringUtilRt;
6 import com.intellij.util.UrlUtilRt;
7 import org.jetbrains.annotations.ApiStatus;
8 import org.jetbrains.annotations.NotNull;
9 import org.jetbrains.annotations.Nullable;
10
11 import java.io.File;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.lang.invoke.MethodHandles;
15 import java.lang.invoke.MethodType;
16 import java.lang.reflect.Field;
17 import java.net.MalformedURLException;
18 import java.net.URL;
19 import java.net.URLClassLoader;
20 import java.nio.ByteBuffer;
21 import java.nio.file.Path;
22 import java.nio.file.Paths;
23 import java.security.ProtectionDomain;
24 import java.util.*;
25 import java.util.function.BiConsumer;
26 import java.util.function.BiPredicate;
27 import java.util.function.Function;
28 import java.util.function.Predicate;
29
30 /**
31  * A class loader that allows for various customizations, e.g. not locking jars or using a special cache to speed up class loading.
32  * Should be constructed using {@link #build()} method.
33  */
34 public class UrlClassLoader extends ClassLoader implements ClassPath.ClassDataConsumer {
35   private static final boolean isParallelCapable = registerAsParallelCapable();
36   private static final ClassLoader appClassLoader = UrlClassLoader.class.getClassLoader();
37
38   private static final ThreadLocal<Boolean> skipFindingResource = new ThreadLocal<>();
39
40   private final List<Path> files;
41   protected final ClassPath classPath;
42   private final ClassLoadingLocks<String> classLoadingLocks;
43   private final boolean isBootstrapResourcesAllowed;
44
45   protected final @NotNull ClassPath.ClassDataConsumer classDataConsumer =
46     ClassPath.recordLoadingTime ? new ClassPath.MeasuringClassDataConsumer(this) : this;
47
48   /**
49    * Called by the VM to support dynamic additions to the class path.
50    *
51    * @see java.lang.instrument.Instrumentation#appendToSystemClassLoaderSearch
52    */
53   @SuppressWarnings("unused")
54   final void appendToClassPathForInstrumentation(@NotNull String jar) {
55     addFiles(Collections.singletonList(Paths.get(jar)));
56   }
57
58   /**
59    * There are two definitions of the `ClassPath` class: one from the app class loader that is used by bootstrap,
60    * and another one from the core class loader produced as a result of creating of plugin class loader.
61    * The core class loader doesn't use bootstrap class loader as a parent - instead, only platform classloader is used (only JRE classes).
62    */
63   @ApiStatus.Internal
64   public final @NotNull ClassPath getClassPath() {
65     return classPath;
66   }
67
68   @ApiStatus.Internal
69   public static @NotNull Collection<Map.Entry<String, Path>> getLoadedClasses() {
70     return ClassPath.getLoadedClasses();
71   }
72
73   /**
74    * See com.intellij.TestAll#getClassRoots()
75    */
76   public final @NotNull List<Path> getBaseUrls() {
77     return classPath.getBaseUrls();
78   }
79
80   // called via reflection
81   @SuppressWarnings({"unused", "MethodMayBeStatic"})
82   public final @NotNull Map<String, Long> getLoadingStats() {
83     return ClassPath.getLoadingStats();
84   }
85
86   public static @NotNull UrlClassLoader.Builder build() {
87     return new Builder();
88   }
89
90   /** @deprecated use {@link #build()} (left for compatibility with `java.system.class.loader` setting) */
91   @Deprecated
92   @ReviseWhenPortedToJDK("9")
93   public UrlClassLoader(@NotNull ClassLoader parent) {
94     this(createDefaultBuilderForJdk(parent), null, isParallelCapable);
95
96     registerInClassLoaderValueMap(parent, this);
97   }
98
99   protected static void registerInClassLoaderValueMap(@NotNull ClassLoader parent, @NotNull ClassLoader classLoader) {
100     // without this ToolProvider.getSystemJavaCompiler() does not work in jdk 9+
101     try {
102       Field f = ClassLoader.class.getDeclaredField("classLoaderValueMap");
103       f.setAccessible(true);
104       f.set(classLoader, f.get(parent));
105     }
106     catch (Exception ignored) { }
107   }
108
109   protected static @NotNull UrlClassLoader.Builder createDefaultBuilderForJdk(@NotNull ClassLoader parent) {
110     Builder configuration = new Builder();
111
112     if (parent instanceof URLClassLoader) {
113       URL[] urls = ((URLClassLoader)parent).getURLs();
114       configuration.files = new ArrayList<>(urls.length);
115       for (URL url : urls) {
116         configuration.files.add(Paths.get(url.getPath()));
117       }
118     }
119     else {
120       String[] parts = System.getProperty("java.class.path").split(System.getProperty("path.separator"));
121       configuration.files = new ArrayList<>(parts.length);
122       for (String s : parts) {
123         configuration.files.add(new File(s).toPath());
124       }
125     }
126
127     configuration.parent = parent.getParent();
128     configuration.lockJars = true;
129     configuration.useCache = true;
130     configuration.isClassPathIndexEnabled = true;
131     configuration.isBootstrapResourcesAllowed = Boolean.parseBoolean(System.getProperty("idea.allow.bootstrap.resources", "true"));
132     configuration.autoAssignUrlsWithProtectionDomain();
133     return configuration;
134   }
135
136   protected UrlClassLoader(@NotNull UrlClassLoader.Builder builder, boolean isParallelCapable) {
137     this(builder, null, isParallelCapable);
138   }
139
140   /**
141    * @deprecated Do not extend UrlClassLoader. If you cannot avoid it, use {@link #UrlClassLoader(Builder, boolean)}.
142    */
143   @ApiStatus.ScheduledForRemoval(inVersion = "2022.1")
144   @Deprecated
145   protected UrlClassLoader(@NotNull UrlClassLoader.Builder builder) {
146     this(builder, null, false);
147   }
148
149   protected UrlClassLoader(@NotNull UrlClassLoader.Builder builder,
150                            @Nullable Function<Path, ResourceFile> resourceFileFactory,
151                            boolean isParallelCapable) {
152     this(builder, resourceFileFactory, isParallelCapable, false);
153   }
154
155   protected UrlClassLoader(@NotNull UrlClassLoader.Builder builder,
156                            @Nullable Function<Path, ResourceFile> resourceFileFactory,
157                            boolean isParallelCapable,
158                            boolean isMimicJarUrlConnectionNeeded) {
159     super(builder.parent);
160
161     files = builder.files;
162
163     Set<Path> urlsWithProtectionDomain = builder.pathsWithProtectionDomain;
164     if (urlsWithProtectionDomain == null) {
165       urlsWithProtectionDomain = Collections.emptySet();
166     }
167
168     classPath = new ClassPath(files, urlsWithProtectionDomain, builder, resourceFileFactory, isMimicJarUrlConnectionNeeded);
169
170     isBootstrapResourcesAllowed = builder.isBootstrapResourcesAllowed;
171     classLoadingLocks = isParallelCapable ? new ClassLoadingLocks<>() : null;
172   }
173
174   protected UrlClassLoader(@NotNull List<Path> files, @NotNull ClassPath classPath) {
175     super(null);
176
177     this.files = files;
178     this.classPath = classPath;
179     isBootstrapResourcesAllowed = false;
180     classLoadingLocks = new ClassLoadingLocks<>();
181   }
182
183   /** @deprecated adding URLs to a classloader at runtime could lead to hard-to-debug errors */
184   @Deprecated
185   public final void addURL(@NotNull URL url) {
186     addFiles(Collections.singletonList(Paths.get(url.getPath())));
187   }
188
189   @ApiStatus.Internal
190   public final void addFiles(@NotNull List<Path> files) {
191     classPath.addFiles(files);
192     this.files.addAll(files);
193   }
194
195   public final @NotNull List<URL> getUrls() {
196     List<URL> result = new ArrayList<>();
197     for (Path file : files) {
198       try {
199         result.add(file.toUri().toURL());
200       }
201       catch (MalformedURLException ignored) { }
202     }
203     return result;
204   }
205
206   public final @NotNull List<Path> getFiles() {
207     return Collections.unmodifiableList(files);
208   }
209
210   public final boolean hasLoadedClass(String name) {
211     Class<?> aClass = findLoadedClass(name);
212     return aClass != null && aClass.getClassLoader() == this;
213   }
214
215   @Override
216   protected Class<?> findClass(@NotNull String name) throws ClassNotFoundException {
217     if (name.startsWith("com.intellij.util.lang.")) {
218       return appClassLoader.loadClass(name);
219     }
220
221     Class<?> clazz;
222     try {
223       clazz = classPath.findClass(name, classDataConsumer);
224     }
225     catch (IOException e) {
226       throw new ClassNotFoundException(name, e);
227     }
228     if (clazz == null) {
229       throw new ClassNotFoundException(name);
230     }
231     return clazz;
232   }
233
234   private void definePackageIfNeeded(String name, Loader loader) throws IOException {
235     int lastDotIndex = name.lastIndexOf('.');
236     if (lastDotIndex == -1) {
237       return;
238     }
239
240     String packageName = name.substring(0, lastDotIndex);
241     // check if the package is already loaded
242     if (isPackageDefined(packageName)) {
243       return;
244     }
245
246     try {
247       Map<Loader.Attribute, String> attributes = loader.getAttributes();
248       if (attributes == null || attributes.isEmpty()) {
249         definePackage(packageName, null, null, null, null, null, null, null);
250       }
251       else {
252         definePackage(packageName,
253                       attributes.get(Loader.Attribute.SPEC_TITLE),
254                       attributes.get(Loader.Attribute.SPEC_VERSION),
255                       attributes.get(Loader.Attribute.SPEC_VENDOR),
256                       attributes.get(Loader.Attribute.IMPL_TITLE),
257                       attributes.get(Loader.Attribute.IMPL_VERSION),
258                       attributes.get(Loader.Attribute.IMPL_VENDOR),
259                       null);
260       }
261     }
262     catch (IllegalArgumentException ignore) {
263       // do nothing, the package is already defined by another thread
264     }
265   }
266
267   @SuppressWarnings("deprecation")
268   protected boolean isPackageDefined(String packageName) {
269     return getPackage(packageName) != null;
270   }
271
272   protected ProtectionDomain getProtectionDomain() {
273     return null;
274   }
275
276   @Override
277   public boolean isByteBufferSupported(@NotNull String name, @Nullable ProtectionDomain protectionDomain) {
278     return true;
279   }
280
281   @Override
282   public Class<?> consumeClassData(@NotNull String name, byte[] data, Loader loader, @Nullable ProtectionDomain protectionDomain) throws IOException {
283     definePackageIfNeeded(name, loader);
284     return super.defineClass(name, data, 0, data.length, protectionDomain == null ? getProtectionDomain() : protectionDomain);
285   }
286
287   @Override
288   public Class<?> consumeClassData(@NotNull String name, ByteBuffer data, Loader loader, @Nullable ProtectionDomain protectionDomain) throws IOException {
289     definePackageIfNeeded(name, loader);
290     return super.defineClass(name, data, protectionDomain == null ? getProtectionDomain() : protectionDomain);
291   }
292
293   @Override
294   public @Nullable URL findResource(@NotNull String name) {
295     if (skipFindingResource.get() != null) {
296       return null;
297     }
298     Resource resource = doFindResource(name);
299     return resource != null ? resource.getURL() : null;
300   }
301
302   @Override
303   public @Nullable InputStream getResourceAsStream(@NotNull String name) {
304     Resource resource = doFindResource(name);
305     if (resource != null) {
306       try {
307         return resource.getInputStream();
308       }
309       catch (IOException e) {
310         logError("Cannot load resource " + name, e);
311         return null;
312       }
313     }
314
315     if (isBootstrapResourcesAllowed) {
316       skipFindingResource.set(Boolean.TRUE);
317       try {
318         URL url = super.getResource(name);
319         if (url != null) {
320           try {
321             return url.openStream();
322           }
323           catch (IOException ignore) { }
324         }
325       }
326       finally {
327         skipFindingResource.set(null);
328       }
329     }
330
331     return null;
332   }
333
334   private @Nullable Resource doFindResource(String name) {
335     String canonicalPath = toCanonicalPath(name);
336     Resource resource = classPath.findResource(canonicalPath);
337     if (resource == null && canonicalPath.startsWith("/") && classPath.findResource(canonicalPath.substring(1)) != null) {
338       logError("Calling `ClassLoader#getResource` with leading slash doesn't work; strip", new IllegalArgumentException(name));
339     }
340     return resource;
341   }
342
343   public final void processResources(@NotNull String dir,
344                                      @NotNull Predicate<? super String> fileNameFilter,
345                                      @NotNull BiConsumer<? super String, ? super InputStream> consumer) throws IOException {
346     classPath.processResources(dir, fileNameFilter, consumer);
347   }
348
349   @Override
350   protected @NotNull Enumeration<URL> findResources(@NotNull String name) throws IOException {
351     return classPath.getResources(name);
352   }
353
354   @Override
355   protected final @NotNull Object getClassLoadingLock(String className) {
356     return classLoadingLocks == null ? this : classLoadingLocks.getOrCreateLock(className);
357   }
358
359   @ApiStatus.Internal
360   public @Nullable BiPredicate<String, Boolean> resolveScopeManager;
361
362   public @Nullable Class<?> loadClassInsideSelf(@NotNull String name, boolean forceLoadFromSubPluginClassloader) throws IOException {
363     synchronized (getClassLoadingLock(name)) {
364       Class<?> c = findLoadedClass(name);
365       if (c != null) {
366         return c;
367       }
368
369       if (!forceLoadFromSubPluginClassloader) {
370         // "self" makes sense for PluginClassLoader, but not for UrlClassLoader - our parent it is implementation detail
371         ClassLoader parent = getParent();
372         if (parent != null) {
373           try {
374             c = parent.loadClass(name);
375           }
376           catch (ClassNotFoundException ignore) { }
377         }
378
379         if (c != null) {
380           return c;
381         }
382       }
383       return classPath.findClass(name, classDataConsumer);
384     }
385   }
386
387   /**
388    * An interface for a pool to store internal caches that can be shared between different class loaders,
389    * if they contain the same URLs in their class paths.
390    * <p>
391    * The implementation is subject to change; one shouldn't rely on it.
392    *
393    * @see #createCachePool()
394    * @see Builder#useCache
395    */
396   public interface CachePool { }
397
398   /**
399    * @return a new pool to be able to share internal caches between different class loaders, if they contain the same URLs
400    * in their class paths.
401    */
402   public static @NotNull CachePool createCachePool() {
403     return new CachePoolImpl();
404   }
405
406   @SuppressWarnings("DuplicatedCode")
407   protected static String toCanonicalPath(@NotNull String path) {
408     if (path.isEmpty()) {
409       return path;
410     }
411
412     if (path.charAt(0) == '.') {
413       if (path.length() == 1) {
414         return "";
415       }
416       char c = path.charAt(1);
417       if (c == '/') {
418         path = path.substring(2);
419       }
420     }
421
422     // trying to speed up the common case when there are no "//" or "/."
423     int index = -1;
424     do {
425       index = path.indexOf('/', index + 1);
426       char next = index == path.length() - 1 ? 0 : path.charAt(index + 1);
427       if (next == '.' || next == '/') {
428         break;
429       }
430     }
431     while (index != -1);
432     if (index == -1) {
433       return path;
434     }
435
436     StringBuilder result = new StringBuilder(path.length());
437     int start = processRoot(path, result);
438     int dots = 0;
439     boolean separator = true;
440
441     for (int i = start; i < path.length(); ++i) {
442       char c = path.charAt(i);
443       if (c == '/') {
444         if (!separator) {
445           processDots(result, dots, start);
446           dots = 0;
447         }
448         separator = true;
449       }
450       else if (c == '.') {
451         if (separator || dots > 0) {
452           ++dots;
453         }
454         else {
455           result.append('.');
456         }
457         separator = false;
458       }
459       else {
460         while (dots > 0) {
461           result.append('.');
462           dots--;
463         }
464         result.append(c);
465         separator = false;
466       }
467     }
468
469     if (dots > 0) {
470       processDots(result, dots, start);
471     }
472     return result.toString();
473   }
474
475   @SuppressWarnings("DuplicatedCode")
476   private static void processDots(StringBuilder result, int dots, int start) {
477     if (dots == 2) {
478       int pos = -1;
479       if (!StringUtilRt.endsWith(result, "/../") && !"../".contentEquals(result)) {
480         pos = StringUtilRt.lastIndexOf(result, '/', start, result.length() - 1);
481         if (pos >= 0) {
482           ++pos;  // separator found, trim to next char
483         }
484         else if (start > 0) {
485           pos = start;  // the path is absolute, trim to root ('/..' -> '/')
486         }
487         else if (result.length() > 0) {
488           pos = 0;  // the path is relative, trim to default ('a/..' -> '')
489         }
490       }
491       if (pos >= 0) {
492         result.delete(pos, result.length());
493       }
494       else {
495         result.append("../");  // impossible to traverse, keep as-is
496       }
497     }
498     else if (dots != 1) {
499       for (int i = 0; i < dots; i++) {
500         result.append('.');
501       }
502       result.append('/');
503     }
504   }
505
506   @SuppressWarnings("DuplicatedCode")
507   private static int processRoot(String path, StringBuilder result) {
508     if (!path.isEmpty() && path.charAt(0) == '/') {
509       result.append('/');
510       return 1;
511     }
512
513     if (path.length() > 2 && path.charAt(1) == ':' && path.charAt(2) == '/') {
514       result.append(path, 0, 3);
515       return 3;
516     }
517
518     return 0;
519   }
520
521   @SuppressWarnings({"UseOfSystemOutOrSystemErr", "SameParameterValue"})
522   private void logError(String message, Throwable t) {
523     try {
524       Class<?> logger = Class.forName("com.intellij.openapi.diagnostic.Logger", false, this);
525       MethodHandles.Lookup lookup = MethodHandles.lookup();
526       Object instance = lookup.findStatic(logger, "getInstance", MethodType.methodType(logger, Class.class)).invoke(getClass());
527       lookup.findVirtual(logger, "error", MethodType.methodType(void.class, String.class, Throwable.class))
528         .bindTo(instance)
529         .invokeExact(message, t);
530     }
531     catch (Throwable tt) {
532       tt.addSuppressed(t);
533       tt.printStackTrace(System.err);
534     }
535   }
536
537   // work around corrupted URLs produced by File.getURL()
538   // public for test
539   public static @NotNull String urlToFilePath(@NotNull String url) {
540     int start = url.startsWith("file:") ? "file:".length() : 0;
541     int end = url.indexOf("!/");
542     if (url.charAt(start) == '/') {
543       // trim leading slashes before drive letter
544       if (url.length() > (start + 2) && url.charAt(start + 2) == ':') {
545         start++;
546       }
547     }
548     return UrlUtilRt.unescapePercentSequences(url, start, end < 0 ? url.length() : end).toString();
549   }
550
551   public static final class Builder {
552     private static final boolean isClassPathIndexEnabledGlobalValue = Boolean.parseBoolean(System.getProperty("idea.classpath.index.enabled", "true"));
553
554     List<Path> files = Collections.emptyList();
555     @Nullable Set<Path> pathsWithProtectionDomain;
556     ClassLoader parent;
557     boolean lockJars = true;
558     boolean useCache;
559     boolean isClassPathIndexEnabled;
560     boolean isBootstrapResourcesAllowed;
561     boolean errorOnMissingJar = true;
562     @Nullable CachePoolImpl cachePool;
563     Predicate<? super Path> cachingCondition;
564
565     Builder() { }
566
567     /**
568      * @deprecated Use {@link #files(List)}. Using of {@link URL} is discouraged in favor of modern {@link Path}.
569      */
570     @ApiStatus.ScheduledForRemoval(inVersion = "2022.2")
571     @Deprecated
572     public @NotNull UrlClassLoader.Builder urls(@NotNull List<URL> urls) {
573       List<Path> files = new ArrayList<>(urls.size());
574       for (URL url : urls) {
575         files.add(Paths.get(urlToFilePath(url.getPath())));
576       }
577       this.files = files;
578       return this;
579     }
580
581     public @NotNull UrlClassLoader.Builder files(@NotNull List<Path> paths) {
582       this.files = paths;
583       return this;
584     }
585
586     /**
587      * Marks URLs that are signed by Sun/Oracle and whose signatures must be verified.
588      */
589     @NotNull UrlClassLoader.Builder urlsWithProtectionDomain(@NotNull Set<Path> value) {
590       pathsWithProtectionDomain = value;
591       return this;
592     }
593
594     public @NotNull UrlClassLoader.Builder parent(ClassLoader parent) {
595       this.parent = parent;
596       return this;
597     }
598
599     /**
600      * `ZipFile` handles opened in `JarLoader` will be kept in as soft references.
601      * Depending on OS, the option significantly speeds up classloading from libraries.
602      * Caveat: on Windows, unclosed handle locks a file, preventing its modification.
603      * Thus, the option is recommended when .jar files are not modified or a process that uses this option is transient.
604      */
605     public @NotNull UrlClassLoader.Builder allowLock(boolean lockJars) {
606       this.lockJars = lockJars;
607       return this;
608     }
609
610     /**
611      * Build a backward index of packages to class/resource names; allows to reduce I/O during classloading.
612      */
613     public @NotNull UrlClassLoader.Builder useCache() {
614       useCache = true;
615       return this;
616     }
617
618     public @NotNull UrlClassLoader.Builder useCache(boolean useCache) {
619       this.useCache = useCache;
620       return this;
621     }
622
623     /**
624      * `FileLoader` will save a list of files/packages under its root and use this information instead of walking files.
625      * Should be used only when the caches can be properly invalidated (when e.g. a new file appears under `FileLoader`'s root).
626      * Currently, the flag is used for faster unit tests / debug IDE instance, because IDEA's build process (as of 14.1) ensures deletion of
627      * such information upon appearing new file for output root.
628      * <p>
629      * N.b. IDEA's build process does not ensure deletion of cached information upon deletion of some file under a local root,
630      * but false positives are not a logical error, since code is prepared for that and disk access is performed upon class/resource loading.
631      */
632     public @NotNull UrlClassLoader.Builder usePersistentClasspathIndexForLocalClassDirectories() {
633       this.isClassPathIndexEnabled = isClassPathIndexEnabledGlobalValue;
634       return this;
635     }
636
637     /**
638      * Requests the class loader being built to use cache and, if possible, retrieve and store the cached data from a special cache pool
639      * that can be shared between several loaders.
640      *
641      * @param pool      cache pool
642      * @param condition a custom policy to provide a possibility to prohibit caching for some URLs.
643      */
644     public @NotNull UrlClassLoader.Builder useCache(@NotNull UrlClassLoader.CachePool pool, @NotNull Predicate<? super Path> condition) {
645       useCache = true;
646       cachePool = (CachePoolImpl)pool;
647       cachingCondition = condition;
648       return this;
649     }
650
651     public @NotNull UrlClassLoader.Builder noPreload() {
652       return this;
653     }
654
655     public @NotNull UrlClassLoader.Builder allowBootstrapResources() {
656       return allowBootstrapResources(true);
657     }
658
659     public @NotNull UrlClassLoader.Builder allowBootstrapResources(boolean allowBootstrapResources) {
660       isBootstrapResourcesAllowed = allowBootstrapResources;
661       return this;
662     }
663
664     public @NotNull UrlClassLoader.Builder setLogErrorOnMissingJar(boolean log) {
665       errorOnMissingJar = log;
666       return this;
667     }
668
669     public @NotNull UrlClassLoader.Builder autoAssignUrlsWithProtectionDomain() {
670       Set<Path> result = null;
671       for (Path path : files) {
672         if (isUrlNeedsProtectionDomain(path)) {
673           if (result == null) {
674             result = new HashSet<>();
675           }
676           result.add(path);
677         }
678       }
679       pathsWithProtectionDomain = result;
680       return this;
681     }
682
683     public @NotNull UrlClassLoader get() {
684       return new UrlClassLoader(this, null, isParallelCapable);
685     }
686
687     private static boolean isUrlNeedsProtectionDomain(@NotNull Path file) {
688       String path = file.toString();
689       // BouncyCastle needs a protection domain
690       if (path.endsWith(".jar")) {
691         int offset = path.lastIndexOf(file.getFileSystem().getSeparator().charAt(0)) + 1;
692         //noinspection SpellCheckingInspection
693         if (path.startsWith("bcprov-", offset) || path.startsWith("bcpkix-", offset)) {
694           return true;
695         }
696       }
697       return false;
698     }
699   }
700 }