001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.geronimo.kernel.config;
018
019 import java.beans.Introspector;
020 import java.io.IOException;
021 import java.io.ObjectInputStream;
022 import java.io.ObjectOutputStream;
023 import java.io.ObjectStreamClass;
024 import java.lang.reflect.Field;
025 import java.net.MalformedURLException;
026 import java.net.URL;
027 import java.net.URLClassLoader;
028 import java.net.URLStreamHandlerFactory;
029 import java.util.ArrayList;
030 import java.util.Collection;
031 import java.util.Collections;
032 import java.util.Enumeration;
033 import java.util.HashSet;
034 import java.util.concurrent.ConcurrentHashMap;
035 import java.util.LinkedList;
036 import java.util.List;
037 import java.util.Map;
038 import java.util.Set;
039
040 import org.apache.commons.logging.Log;
041 import org.apache.commons.logging.LogFactory;
042 import org.apache.geronimo.kernel.classloader.UnionEnumeration;
043 import org.apache.geronimo.kernel.repository.Artifact;
044 import org.apache.geronimo.kernel.util.ClassLoaderRegistry;
045
046 /**
047 * A MultiParentClassLoader is a simple extension of the URLClassLoader that simply changes the single parent class
048 * loader model to support a list of parent class loaders. Each operation that accesses a parent, has been replaced
049 * with a operation that checks each parent in order. This getParent method of this class will always return null,
050 * which may be interpreted by the calling code to mean that this class loader is a direct child of the system class
051 * loader.
052 *
053 * @version $Rev: 983505 $ $Date: 2010-08-09 10:36:35 +0800 (Mon, 09 Aug 2010) $
054 */
055 public class MultiParentClassLoader extends URLClassLoader {
056 private static final Log log = LogFactory.getLog(MultiParentClassLoader.class);
057 private final Artifact id;
058 private final ClassLoader[] parents;
059 private final boolean inverseClassLoading;
060 private final String[] hiddenClasses;
061 private final String[] nonOverridableClasses;
062 private final String[] hiddenResources;
063 private final String[] nonOverridableResources;
064 private boolean destroyed = false;
065 private Map<String,Object> resourcesNotFound = new ConcurrentHashMap<String,Object>();
066
067 // I used this pattern as its temporary and with the static final we get compile time
068 // optimizations.
069 private final static int classLoaderSearchMode;
070 private final static int ORIGINAL_SEARCH = 1;
071 private final static int OPTIMIZED_SEARCH = 2;
072
073 static {
074 // Extract the classLoaderSearchMode if specified. If not, default to "safe".
075 String mode = System.getProperty("Xorg.apache.geronimo.kernel.config.MPCLSearchOption");
076 int runtimeMode = OPTIMIZED_SEARCH; // Default to optimized
077 String runtimeModeMessage = "Original Classloading";
078 if (mode != null) {
079 if (mode.equals("safe")) {
080 runtimeMode = ORIGINAL_SEARCH;
081 runtimeModeMessage = "Safe ClassLoading";
082 } else if (mode.equals("optimized"))
083 runtimeMode = OPTIMIZED_SEARCH;
084 }
085
086 classLoaderSearchMode = runtimeMode;
087 log.info("ClassLoading behaviour has changed. The "+runtimeModeMessage+" mode is in effect. If you are experiencing a problem\n"+
088 "you can change the behaviour by specifying -DXorg.apache.geronimo.kernel.config.MPCLSearchOption= property. Specify \n"+
089 "=\"safe\" to revert to the original behaviour. This is a temporary change until we decide whether or not to make it\n"+
090 "permanent for the 2.0 release");
091 }
092
093 /**
094 * Creates a named class loader with no parents.
095 *
096 * @param id the id of this class loader
097 * @param urls the urls from which this class loader will classes and resources
098 */
099 public MultiParentClassLoader(Artifact id, URL[] urls) {
100 super(urls);
101 this.id = id;
102 parents = new ClassLoader[]{ClassLoader.getSystemClassLoader()};
103 inverseClassLoading = false;
104 hiddenClasses = new String[0];
105 nonOverridableClasses = new String[0];
106 hiddenResources = new String[0];
107 nonOverridableResources = new String[0];
108 ClassLoaderRegistry.add(this);
109 }
110
111
112 /**
113 * Creates a named class loader as a child of the specified parent.
114 *
115 * @param id the id of this class loader
116 * @param urls the urls from which this class loader will classes and resources
117 * @param parent the parent of this class loader
118 */
119 public MultiParentClassLoader(Artifact id, URL[] urls, ClassLoader parent) {
120 this(id, urls, new ClassLoader[]{parent});
121 }
122
123 public MultiParentClassLoader(Artifact id, URL[] urls, ClassLoader parent, boolean inverseClassLoading, String[] hiddenClasses, String[] nonOverridableClasses) {
124 this(id, urls, new ClassLoader[]{parent}, inverseClassLoading, hiddenClasses, nonOverridableClasses);
125 }
126
127 /**
128 * Creates a named class loader as a child of the specified parent and using the specified URLStreamHandlerFactory
129 * for accessing the urls..
130 *
131 * @param id the id of this class loader
132 * @param urls the urls from which this class loader will classes and resources
133 * @param parent the parent of this class loader
134 * @param factory the URLStreamHandlerFactory used to access the urls
135 */
136 public MultiParentClassLoader(Artifact id, URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
137 this(id, urls, new ClassLoader[]{parent}, factory);
138 }
139
140 /**
141 * Creates a named class loader as a child of the specified parents.
142 *
143 * @param id the id of this class loader
144 * @param urls the urls from which this class loader will classes and resources
145 * @param parents the parents of this class loader
146 */
147 public MultiParentClassLoader(Artifact id, URL[] urls, ClassLoader[] parents) {
148 super(urls);
149 this.id = id;
150 this.parents = copyParents(parents);
151 inverseClassLoading = false;
152 hiddenClasses = new String[0];
153 nonOverridableClasses = new String[0];
154 hiddenResources = new String[0];
155 nonOverridableResources = new String[0];
156 ClassLoaderRegistry.add(this);
157 }
158
159 public MultiParentClassLoader(Artifact id, URL[] urls, ClassLoader[] parents, boolean inverseClassLoading, Collection hiddenClasses, Collection nonOverridableClasses) {
160 this(id, urls, parents, inverseClassLoading, (String[]) hiddenClasses.toArray(new String[hiddenClasses.size()]), (String[]) nonOverridableClasses.toArray(new String[nonOverridableClasses.size()]));
161 }
162
163 public MultiParentClassLoader(Artifact id, URL[] urls, ClassLoader[] parents, boolean inverseClassLoading, String[] hiddenClasses, String[] nonOverridableClasses) {
164 super(urls);
165 this.id = id;
166 this.parents = copyParents(parents);
167 this.inverseClassLoading = inverseClassLoading;
168 this.hiddenClasses = hiddenClasses;
169 this.nonOverridableClasses = nonOverridableClasses;
170 hiddenResources = toResources(hiddenClasses);
171 nonOverridableResources = toResources(nonOverridableClasses);
172 ClassLoaderRegistry.add(this);
173 }
174
175 public MultiParentClassLoader(MultiParentClassLoader source) {
176 this(source.id, source.getURLs(), deepCopyParents(source.parents), source.inverseClassLoading, source.hiddenClasses, source.nonOverridableClasses);
177 }
178
179 static ClassLoader copy(ClassLoader source) {
180 if (source instanceof MultiParentClassLoader) {
181 return new MultiParentClassLoader((MultiParentClassLoader) source);
182 } else if (source instanceof URLClassLoader) {
183 return new URLClassLoader(((URLClassLoader) source).getURLs(), source.getParent());
184 } else {
185 return new URLClassLoader(new URL[0], source);
186 }
187 }
188
189 ClassLoader copy() {
190 return MultiParentClassLoader.copy(this);
191 }
192
193 private String[] toResources(String[] classes) {
194 String[] resources = new String[classes.length];
195 for (int i = 0; i < classes.length; i++) {
196 String className = classes[i];
197 resources[i] = className.replace('.', '/');
198 }
199 return resources;
200 }
201
202 /**
203 * Creates a named class loader as a child of the specified parents and using the specified URLStreamHandlerFactory
204 * for accessing the urls..
205 *
206 * @param id the id of this class loader
207 * @param urls the urls from which this class loader will classes and resources
208 * @param parents the parents of this class loader
209 * @param factory the URLStreamHandlerFactory used to access the urls
210 */
211 public MultiParentClassLoader(Artifact id, URL[] urls, ClassLoader[] parents, URLStreamHandlerFactory factory) {
212 super(urls, null, factory);
213 this.id = id;
214 this.parents = copyParents(parents);
215 inverseClassLoading = false;
216 hiddenClasses = new String[0];
217 nonOverridableClasses = new String[0];
218 hiddenResources = new String[0];
219 nonOverridableResources = new String[0];
220 ClassLoaderRegistry.add(this);
221 }
222
223 private static ClassLoader[] copyParents(ClassLoader[] parents) {
224 ClassLoader[] newParentsArray = new ClassLoader[parents.length];
225 for (int i = 0; i < parents.length; i++) {
226 ClassLoader parent = parents[i];
227 if (parent == null) {
228 throw new NullPointerException("parent[" + i + "] is null");
229 }
230 newParentsArray[i] = parent;
231 }
232 return newParentsArray;
233 }
234
235 private static ClassLoader[] deepCopyParents(ClassLoader[] parents) {
236 ClassLoader[] newParentsArray = new ClassLoader[parents.length];
237 for (int i = 0; i < parents.length; i++) {
238 ClassLoader parent = parents[i];
239 if (parent == null) {
240 throw new NullPointerException("parent[" + i + "] is null");
241 }
242 if (parent instanceof MultiParentClassLoader) {
243 parent = ((MultiParentClassLoader) parent).copy();
244 }
245 newParentsArray[i] = parent;
246 }
247 return newParentsArray;
248 }
249
250 /**
251 * Gets the id of this class loader.
252 *
253 * @return the id of this class loader
254 */
255 public Artifact getId() {
256 return id;
257 }
258
259 /**
260 * Gets the parents of this class loader.
261 *
262 * @return the parents of this class loader
263 */
264 public ClassLoader[] getParents() {
265 return parents;
266 }
267
268 public void addURL(URL url) {
269 // todo this needs a security check
270 super.addURL(url);
271 }
272
273 /**
274 * TODO This method should be removed and replaced with the best classLoading option. Its intent is to
275 * provide a way for folks to switch back to the old classLoader if this fix breaks something.
276 */
277 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
278 if (classLoaderSearchMode == ORIGINAL_SEARCH)
279 return loadSafeClass(name, resolve);
280 else
281 return loadOptimizedClass(name, resolve);
282 }
283
284 /**
285 * This method executes the old class loading behaviour before optimization.
286 *
287 * @param name
288 * @param resolve
289 * @return
290 * @throws ClassNotFoundException
291 */
292 protected synchronized Class<?> loadSafeClass(String name, boolean resolve) throws ClassNotFoundException {
293 //
294 // Check if class is in the loaded classes cache
295 //
296 Class cachedClass = findLoadedClass(name);
297 if (cachedClass != null) {
298 return resolveClass(cachedClass, resolve);
299 }
300
301 // This is a reasonable hack. We can add some classes to the list below.
302 // Since we know these classes are in the system class loader let's not waste our
303 // time going through the hierarchy.
304 //
305 // The order is based on profiling the server. It may not be optimal for all
306 // workloads.
307
308 if (name.startsWith("java.")) {
309 Class clazz = ClassLoader.getSystemClassLoader().loadClass(name);
310 return resolveClass(clazz, resolve);
311 }
312
313 //
314 // if we are using inverse class loading, check local urls first
315 //
316 if (inverseClassLoading && !isDestroyed() && !isNonOverridableClass(name)) {
317 try {
318 Class clazz = findClass(name);
319 return resolveClass(clazz, resolve);
320 } catch (ClassNotFoundException ignored) {
321 }
322 }
323
324 //
325 // Check parent class loaders
326 //
327 if (!isHiddenClass(name)) {
328 for (ClassLoader parent : parents) {
329 try {
330 Class clazz = parent.loadClass(name);
331 return resolveClass(clazz, resolve);
332 } catch (ClassNotFoundException ignored) {
333 // this parent didn't have the class; try the next one
334 }
335 }
336 }
337
338 //
339 // if we are not using inverse class loading, check local urls now
340 //
341 // don't worry about excluding non-overridable classes here... we
342 // have alredy checked he parent and the parent didn't have the
343 // class, so we can override now
344 if (!isDestroyed()) {
345 try {
346 Class clazz = findClass(name);
347 return resolveClass(clazz, resolve);
348 } catch (ClassNotFoundException ignored) {
349 }
350 }
351
352 throw new ClassNotFoundException(name + " in classloader " + id);
353 }
354
355 /**
356 *
357 * Optimized classloading.
358 *
359 * This method is the normal way to resolve class loads. This method recursively calls its parents to resolve
360 * classloading requests. Here is the sequence of operations:
361 *
362 * 1. Call findLoadedClass to see if we already have this class loaded.
363 * 2. If this class is a java.* or data primitive class, call the SystemClassLoader.
364 * 3. If inverse loading and class is not in the non-overridable list, check the local ClassLoader.
365 * 4. If the class is not a hidden class, search our parents, recursively. Keeping track of which parents have already been called.
366 * Since MultiParentClassLoaders can appear more than once we do not search an already searched ClassLoader.
367 * 5. Finally, search this ClassLoader.
368 *
369 */
370 protected synchronized Class<?> loadOptimizedClass(String name, boolean resolve) throws ClassNotFoundException {
371
372 //
373 // Check if class is in the loaded classes cache
374 //
375 Class cachedClass = findLoadedClass(name);
376 if (cachedClass != null) {
377 return resolveClass(cachedClass, resolve);
378 }
379
380 //
381 // If this is a java.* or primitive class, use the primordial ClassLoader...
382 //
383 // The order is based on profiling the server. It may not be optimal for all
384 // workloads.
385 if (name.startsWith("java.")) {
386 try {
387 return resolveClass(findSystemClass(name), resolve);
388 } catch (ClassNotFoundException cnfe) {
389 // ignore...just being a good citizen.
390 }
391 }
392
393 //
394 // if we are using inverse class loading, check local urls first
395 //
396 if (inverseClassLoading && !isDestroyed() && !isNonOverridableClass(name)) {
397 try {
398 Class clazz = findClass(name);
399 return resolveClass(clazz, resolve);
400 } catch (ClassNotFoundException ignored) {
401 }
402 }
403
404 //
405 // Check parent class loaders
406 //
407 if (!isHiddenClass(name)) {
408 try {
409 LinkedList<ClassLoader> visitedClassLoaders = new LinkedList<ClassLoader>();
410 Class clazz = checkParents(name, resolve, visitedClassLoaders);
411 if (clazz != null) return resolveClass(clazz, resolve);
412 } catch (ClassNotFoundException cnfe) {
413 // ignore
414 }
415 }
416
417 //
418 // if we are not using inverse class loading, check local urls now
419 //
420 // don't worry about excluding non-overridable classes here... we
421 // have alredy checked he parent and the parent didn't have the
422 // class, so we can override now
423 if (!isDestroyed()) {
424 try {
425 Class clazz = findClass(name);
426 return resolveClass(clazz, resolve);
427 } catch (ClassNotFoundException ignored) {
428 }
429 }
430
431 throw new ClassNotFoundException(name + " in classloader " + id);
432 }
433
434 /**
435 * This method is an internal hook that allows us to be performant on Class lookups when multiparent
436 * classloaders are involved. We can bypass certain lookups that have already occurred in the initiating
437 * classloader. Also, we track the classLoaders that are visited by adding them to an already vistied list.
438 * In this way, we can bypass redundant checks for the same class.
439 *
440 * @param name
441 * @param visitedClassLoaders
442 * @return
443 * @throws ClassNotFoundException
444 */
445 protected synchronized Class<?> loadClassInternal(String name, boolean resolve, LinkedList<ClassLoader> visitedClassLoaders) throws ClassNotFoundException, MalformedURLException {
446 //
447 // Check if class is in the loaded classes cache
448 //
449 Class cachedClass = findLoadedClass(name);
450 if (cachedClass != null) {
451 return resolveClass(cachedClass, resolve);
452 }
453
454 //
455 // Check parent class loaders
456 //
457 if (!isHiddenClass(name)) {
458 try {
459 Class clazz = checkParents(name, resolve, visitedClassLoaders);
460 if (clazz != null) return resolveClass(clazz,resolve);
461 } catch (ClassNotFoundException cnfe) {
462 // ignore
463 }
464 }
465
466 //
467 // if we are not using inverse class loading, check local urls now
468 //
469 // don't worry about excluding non-overridable classes here... we
470 // have alredy checked he parent and the parent didn't have the
471 // class, so we can override now
472 if (!isDestroyed()) {
473 Class clazz = findClass(name);
474 return resolveClass(clazz, resolve);
475 }
476
477 return null; // Caller is expecting a class. Null indicates CNFE and will save some time.
478 }
479
480 /**
481 * In order to optimize the classLoading process and visit a directed set of
482 * classloaders this internal method for Geronimo MultiParentClassLoaders
483 * is used. Effectively, as each classloader is visited it is passed a linked
484 * list of classloaders that have already been visited and can safely be skipped.
485 * This method assumes the context of an MPCL and is not for use external to this class.
486 *
487 * @param name
488 * @param visitedClassLoaders
489 * @return
490 * @throws ClassNotFoundException
491 */
492 private synchronized Class<?> checkParents(String name, boolean resolve, LinkedList<ClassLoader> visitedClassLoaders) throws ClassNotFoundException {
493 for (ClassLoader parent : parents) {
494 if (!visitedClassLoaders.contains(parent)) {
495 visitedClassLoaders.add(parent); // Track that we've been here before
496 try {
497 if (parent instanceof MultiParentClassLoader) {
498 Class clazz = ((MultiParentClassLoader) parent).loadClassInternal(name, resolve, visitedClassLoaders);
499 if (clazz != null) return resolveClass(clazz, resolve);
500 } else {
501 return parent.loadClass(name);
502 }
503 } catch (ClassNotFoundException cnfe) {
504 // ignore
505 } catch (MalformedURLException me) {
506 log.debug("Failed findClass=" + name, me);
507 }
508 }
509 }
510 // To avoid yet another CNFE we'll simply return null and let the caller handle appropriately.
511 return null;
512 }
513
514 private boolean isNonOverridableClass(String name) {
515 for (String nonOverridableClass : nonOverridableClasses) {
516 if (name.startsWith(nonOverridableClass)) {
517 return true;
518 }
519 }
520 return false;
521 }
522
523 private boolean isHiddenClass(String name) {
524 for (String hiddenClass : hiddenClasses) {
525 if (name.startsWith(hiddenClass)) {
526 return true;
527 }
528 }
529 return false;
530 }
531
532 private Class resolveClass(Class clazz, boolean resolve) {
533 if (resolve) {
534 resolveClass(clazz);
535 }
536 return clazz;
537 }
538
539 public URL getResource(String name) {
540 if (isDestroyed() || resourcesNotFound.containsKey(name)) {
541 return null;
542 }
543
544 //
545 // if we are using inverse class loading, check local urls first
546 //
547 if (inverseClassLoading && !isDestroyed() && !isNonOverridableResource(name)) {
548 URL url = findResource(name);
549 if (url != null) {
550 return url;
551 }
552 }
553
554 //
555 // Check parent class loaders
556 //
557 if (!isHiddenResource(name)) {
558 for (ClassLoader parent : parents) {
559 URL url = parent.getResource(name);
560 if (url != null) {
561 return url;
562 }
563 }
564 }
565
566 //
567 // if we are not using inverse class loading, check local urls now
568 //
569 // don't worry about excluding non-overridable resources here... we
570 // have alredy checked he parent and the parent didn't have the
571 // resource, so we can override now
572 if (!isDestroyed()) {
573 // parents didn't have the resource; attempt to load it from my urls
574 URL url = findResource(name);
575 if (url != null) {
576 return url;
577 }
578 }
579
580 //
581 // Resource not found -- no need to search for it again
582 // Use the name as key and value. We don't care about the value and it needs to be non-null.
583 //
584 resourcesNotFound.put(name, name);
585
586 return null;
587 }
588
589 public Enumeration<URL> findResources(String name) throws IOException {
590 if (isDestroyed()) {
591 return Collections.enumeration(Collections.EMPTY_SET);
592 }
593
594 Set<ClassLoader> knownClassloaders = new HashSet<ClassLoader>();
595 List<Enumeration<URL>> enumerations = new ArrayList<Enumeration<URL>>();
596
597 recursiveFind(knownClassloaders, enumerations, name);
598
599 return new UnionEnumeration<URL>(enumerations);
600 }
601
602 protected void recursiveFind(Set<ClassLoader> knownClassloaders, List<Enumeration<URL>> enumerations, String name) throws IOException {
603 if (isDestroyed() || knownClassloaders.contains(this)) {
604 return;
605 }
606 knownClassloaders.add(this);
607 if (inverseClassLoading && !isNonOverridableResource(name)) {
608 enumerations.add(internalfindResources(name));
609 }
610 if (!isHiddenResource(name)) {
611 for (ClassLoader parent : parents) {
612 if (parent instanceof MultiParentClassLoader) {
613 ((MultiParentClassLoader) parent).recursiveFind(knownClassloaders, enumerations, name);
614 } else {
615 if (!knownClassloaders.contains(parent)) {
616 enumerations.add(parent.getResources(name));
617 knownClassloaders.add(parent);
618 }
619 }
620 }
621 }
622 if (!inverseClassLoading) {
623 enumerations.add(internalfindResources(name));
624 }
625 }
626
627 protected Enumeration<URL> internalfindResources(String name) throws IOException {
628 return super.findResources(name);
629 }
630
631 private boolean isNonOverridableResource(String name) {
632 for (String nonOverridableResource : nonOverridableResources) {
633 if (name.startsWith(nonOverridableResource)) {
634 return true;
635 }
636 }
637 return false;
638 }
639
640 private boolean isHiddenResource(String name) {
641 for (String hiddenResource : hiddenResources) {
642 if (name.startsWith(hiddenResource)) {
643 return true;
644 }
645 }
646 return false;
647 }
648
649 public String toString() {
650 return "[" + getClass().getName() + " id=" + id + "]";
651 }
652
653 public synchronized boolean isDestroyed() {
654 return destroyed;
655 }
656
657 public void destroy() {
658 synchronized (this) {
659 if (destroyed) return;
660 destroyed = true;
661 }
662
663 LogFactory.release(this);
664 clearSoftCache(ObjectInputStream.class, "subclassAudits");
665 clearSoftCache(ObjectOutputStream.class, "subclassAudits");
666 clearSoftCache(ObjectStreamClass.class, "localDescs");
667 clearSoftCache(ObjectStreamClass.class, "reflectors");
668
669 // The beanInfoCache in java.beans.Introspector will hold on to Classes which
670 // it has introspected. If we don't flush the cache, we may run out of
671 // Permanent Generation space.
672 Introspector.flushCaches();
673
674 ClassLoaderRegistry.remove(this);
675 }
676
677 private static final Object lock = new Object();
678 private static boolean clearSoftCacheFailed = false;
679
680 private static void clearSoftCache(Class clazz, String fieldName) {
681 Map cache = null;
682 try {
683 Field f = clazz.getDeclaredField(fieldName);
684 f.setAccessible(true);
685 cache = (Map) f.get(null);
686 } catch (Throwable e) {
687 synchronized (lock) {
688 if (!clearSoftCacheFailed) {
689 clearSoftCacheFailed = true;
690 LogFactory.getLog(MultiParentClassLoader.class).debug("Unable to clear SoftCache field " + fieldName + " in class " + clazz);
691 }
692 }
693 }
694
695 if (cache != null) {
696 synchronized (cache) {
697 cache.clear();
698 }
699 }
700 }
701
702 protected void finalize() throws Throwable {
703 ClassLoaderRegistry.remove(this);
704 super.finalize();
705 }
706
707 }