1   //========================================================================
2   //Copyright 2004 Mort Bay Consulting Pty. Ltd.
3   //------------------------------------------------------------------------
4   //Licensed under the Apache License, Version 2.0 (the "License");
5   //you may not use this file except in compliance with the License.
6   //You may obtain a copy of the License at
7   //http://www.apache.org/licenses/LICENSE-2.0
8   //Unless required by applicable law or agreed to in writing, software
9   //distributed under the License is distributed on an "AS IS" BASIS,
10  //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  //See the License for the specific language governing permissions and
12  //limitations under the License.
13  //========================================================================
14  
15  package org.mortbay.management;
16  
17  import java.lang.reflect.Array;
18  import java.lang.reflect.Constructor;
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Modifier;
22  import java.util.Enumeration;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.MissingResourceException;
29  import java.util.ResourceBundle;
30  import java.util.Set;
31  
32  import javax.management.Attribute;
33  import javax.management.AttributeList;
34  import javax.management.AttributeNotFoundException;
35  import javax.management.DynamicMBean;
36  import javax.management.InvalidAttributeValueException;
37  import javax.management.MBeanAttributeInfo;
38  import javax.management.MBeanConstructorInfo;
39  import javax.management.MBeanException;
40  import javax.management.MBeanInfo;
41  import javax.management.MBeanNotificationInfo;
42  import javax.management.MBeanOperationInfo;
43  import javax.management.MBeanParameterInfo;
44  import javax.management.ObjectName;
45  import javax.management.ReflectionException;
46  import javax.management.modelmbean.ModelMBean;
47  
48  import org.mortbay.log.Log;
49  import org.mortbay.log.StdErrLog;
50  import org.mortbay.util.LazyList;
51  import org.mortbay.util.Loader;
52  import org.mortbay.util.TypeUtil;
53  
54  /* ------------------------------------------------------------ */
55  /** ObjectMBean.
56   * A dynamic MBean that can wrap an arbitary Object instance.
57   * the attributes and methods exposed by this bean are controlled by
58   * the merge of property bundles discovered by names related to all
59   * superclasses and all superinterfaces.
60   *
61   * Attributes and methods exported may be "Object" and must exist on the
62   * wrapped object, or "MBean" and must exist on a subclass of OBjectMBean
63   * or "MObject" which exists on the wrapped object, but whose values are
64   * converted to MBean object names.
65   *
66   */
67  public class ObjectMBean implements DynamicMBean
68  {
69      private static Class[] OBJ_ARG = new Class[]{Object.class};
70  
71      protected Object _managed;
72      private MBeanInfo _info;
73      private Map _getters=new HashMap();
74      private Map _setters=new HashMap();
75      private Map _methods=new HashMap();
76      private Set _convert=new HashSet();
77      private ClassLoader _loader;
78      private MBeanContainer _mbeanContainer;
79  
80      private static String OBJECT_NAME_CLASS = ObjectName.class.getName();
81      private static String OBJECT_NAME_ARRAY_CLASS = ObjectName[].class.getName();
82  
83      /* ------------------------------------------------------------ */
84      /**
85       * Create MBean for Object. Attempts to create an MBean for the object by searching the package
86       * and class name space. For example an object of the type
87       *
88       * <PRE>
89       * class com.acme.MyClass extends com.acme.util.BaseClass implements com.acme.Iface
90       * </PRE>
91       *
92       * Then this method would look for the following classes:
93       * <UL>
94       * <LI>com.acme.management.MyClassMBean
95       * <LI>com.acme.util.management.BaseClassMBean
96       * <LI>org.mortbay.management.ObjectMBean
97       * </UL>
98       *
99       * @param o The object
100      * @return A new instance of an MBean for the object or null.
101      */
102     public static Object mbeanFor(Object o)
103     {
104         try
105         {
106             Class oClass = o.getClass();
107             Object mbean = null;
108 
109             while (mbean == null && oClass != null)
110             {
111                 String pName = oClass.getPackage().getName();
112                 String cName = oClass.getName().substring(pName.length() + 1);
113                 String mName = pName + ".management." + cName + "MBean";
114                 
115 
116                 try
117                 {
118                     Class mClass = (Object.class.equals(oClass))?oClass=ObjectMBean.class:Loader.loadClass(oClass,mName,true);
119                     if (Log.isDebugEnabled())
120                         Log.debug("mbeanFor " + o + " mClass=" + mClass);
121 
122                     try
123                     {
124                         Constructor constructor = mClass.getConstructor(OBJ_ARG);
125                         mbean=constructor.newInstance(new Object[]{o});
126                     }
127                     catch(Exception e)
128                     {
129                         Log.ignore(e);
130                         if (ModelMBean.class.isAssignableFrom(mClass))
131                         {
132                             mbean=mClass.newInstance();
133                             ((ModelMBean)mbean).setManagedResource(o, "objectReference");
134                         }
135                     }
136 
137                     if (Log.isDebugEnabled())
138                         Log.debug("mbeanFor " + o + " is " + mbean);
139                     return mbean;
140                 }
141                 catch (ClassNotFoundException e)
142                 {
143                     if (e.toString().endsWith("MBean"))
144                         Log.ignore(e);
145                     else
146                         Log.warn(e);
147                 }
148                 catch (Error e)
149                 {
150                     Log.warn(e);
151                     mbean = null;
152                 }
153                 catch (Exception e)
154                 {
155                     Log.warn(e);
156                     mbean = null;
157                 }
158 
159                 oClass = oClass.getSuperclass();
160             }
161         }
162         catch (Exception e)
163         {
164             Log.ignore(e);
165         }
166         return null;
167     }
168 
169 
170     public ObjectMBean(Object managedObject)
171     {
172         _managed = managedObject;
173         _loader = Thread.currentThread().getContextClassLoader();
174     }
175     
176     public Object getManagedObject()
177     {
178         return _managed;
179     }
180     
181     public ObjectName getObjectName()
182     {
183         return null;
184     }
185     
186     public String getObjectNameBasis()
187     {
188         return null;
189     }
190 
191     protected void setMBeanContainer(MBeanContainer container)
192     {
193        this._mbeanContainer = container;
194     }
195 
196     public MBeanContainer getMBeanContainer ()
197     {
198         return this._mbeanContainer;
199     }
200     
201     
202     public MBeanInfo getMBeanInfo()
203     {
204         try
205         {
206             if (_info==null)
207             {
208                 // Start with blank lazy lists attributes etc.
209                 String desc=null;
210                 Object attributes=null;
211                 Object constructors=null;
212                 Object operations=null;
213                 Object notifications=null;
214 
215                 // Find list of classes that can influence the mbean
216                 Class o_class=_managed.getClass();
217                 Object influences = findInfluences(null, _managed.getClass());
218 
219                 // Set to record defined items
220                 Set defined=new HashSet();
221 
222                 // For each influence
223                 for (int i=0;i<LazyList.size(influences);i++)
224                 {
225                     Class oClass = (Class)LazyList.get(influences, i);
226 
227                     // look for a bundle defining methods
228                     if (Object.class.equals(oClass))
229                         oClass=ObjectMBean.class;
230                     String pName = oClass.getPackage().getName();
231                     String cName = oClass.getName().substring(pName.length() + 1);
232                     String rName = pName.replace('.', '/') + "/management/" + cName+"-mbean";
233 
234                     try
235                     {
236                         Log.debug(rName);
237                         ResourceBundle bundle = Loader.getResourceBundle(o_class, rName,true,Locale.getDefault());
238 
239                         
240                         // Extract meta data from bundle
241                         Enumeration e = bundle.getKeys();
242                         while (e.hasMoreElements())
243                         {
244                             String key = (String)e.nextElement();
245                             String value = bundle.getString(key);
246 
247                             // Determin if key is for mbean , attribute or for operation
248                             if (key.equals(cName))
249                             {
250                                 // set the mbean description
251                                 if (desc==null)
252                                     desc=value;
253                             }
254                             else if (key.indexOf('(')>0)
255                             {
256                                 // define an operation
257                                 if (!defined.contains(key) && key.indexOf('[')<0)
258                                 {
259                                     defined.add(key);
260                                     operations=LazyList.add(operations,defineOperation(key, value, bundle));
261                                 }
262                             }
263                             else
264                             {
265                                 // define an attribute
266                                 if (!defined.contains(key))
267                                 {
268                                     defined.add(key);
269                                     attributes=LazyList.add(attributes,defineAttribute(key, value));
270                                 }
271                             }
272                         }
273 
274                     }
275                     catch(MissingResourceException e)
276                     {
277                         Log.ignore(e);
278                     }
279                 }
280 
281                 _info = new MBeanInfo(o_class.getName(),
282                                 desc,
283                                 (MBeanAttributeInfo[])LazyList.toArray(attributes, MBeanAttributeInfo.class),
284                                 (MBeanConstructorInfo[])LazyList.toArray(constructors, MBeanConstructorInfo.class),
285                                 (MBeanOperationInfo[])LazyList.toArray(operations, MBeanOperationInfo.class),
286                                 (MBeanNotificationInfo[])LazyList.toArray(notifications, MBeanNotificationInfo.class));
287             }
288         }
289         catch(RuntimeException e)
290         {
291             Log.warn(e);
292             throw e;
293         }
294         return _info;
295     }
296 
297 
298     /* ------------------------------------------------------------ */
299     public Object getAttribute(String name) throws AttributeNotFoundException, MBeanException, ReflectionException
300     {
301         Method getter = (Method) _getters.get(name);
302         if (getter == null)
303             throw new AttributeNotFoundException(name);
304         try
305         {
306             Object o = _managed;
307             if (getter.getDeclaringClass().isInstance(this))
308                 o = this; // mbean method
309 
310             // get the attribute
311             Object r=getter.invoke(o, (java.lang.Object[]) null);
312 
313             // convert to ObjectName if need be.
314             if (r!=null && _convert.contains(name))
315             {
316                 if (r.getClass().isArray())
317                 {
318                     ObjectName[] on = new ObjectName[Array.getLength(r)];
319                     for (int i=0;i<on.length;i++)
320                         on[i]=_mbeanContainer.findMBean(Array.get(r, i));
321                     r=on;
322                 }
323                 else
324                 {
325                     ObjectName mbean = _mbeanContainer.findMBean(r);
326                     if (mbean==null)
327                         return null;
328                     r=mbean;
329                 }
330             }
331             return r;
332         }
333         catch (IllegalAccessException e)
334         {
335             Log.warn(Log.EXCEPTION, e);
336             throw new AttributeNotFoundException(e.toString());
337         }
338         catch (InvocationTargetException e)
339         {
340             Log.warn(Log.EXCEPTION, e);
341             throw new ReflectionException((Exception) e.getTargetException());
342         }
343     }
344 
345     /* ------------------------------------------------------------ */
346     public AttributeList getAttributes(String[] names)
347     {
348         AttributeList results = new AttributeList(names.length);
349         for (int i = 0; i < names.length; i++)
350         {
351             try
352             {
353                 results.add(new Attribute(names[i], getAttribute(names[i])));
354             }
355             catch (Exception e)
356             {
357                 Log.warn(Log.EXCEPTION, e);
358             }
359         }
360         return results;
361     }
362 
363     /* ------------------------------------------------------------ */
364     public void setAttribute(Attribute attr) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException
365     {
366         if (attr == null)
367             return;
368 
369         if (Log.isDebugEnabled())
370             Log.debug("setAttribute " + _managed + ":" +attr.getName() + "=" + attr.getValue());
371         Method setter = (Method) _setters.get(attr.getName());
372         if (setter == null)
373             throw new AttributeNotFoundException(attr.getName());
374         try
375         {
376             Object o = _managed;
377             if (setter.getDeclaringClass().isInstance(this))
378                 o = this;
379 
380             // get the value
381             Object value = attr.getValue();
382 
383             // convert from ObjectName if need be
384             if (value!=null && _convert.contains(attr.getName()))
385             {
386                 if (value.getClass().isArray())
387                 {
388                     Class t=setter.getParameterTypes()[0].getComponentType();
389                     Object na = Array.newInstance(t,Array.getLength(value));
390                     for (int i=Array.getLength(value);i-->0;)
391                         Array.set(na, i, _mbeanContainer.findBean((ObjectName)Array.get(value, i)));
392                     value=na;
393                 }
394                 else
395                     value=_mbeanContainer.findBean((ObjectName)value);
396             }
397 
398             // do the setting
399             setter.invoke(o, new Object[]{ value });
400         }
401         catch (IllegalAccessException e)
402         {
403             Log.warn(Log.EXCEPTION, e);
404             throw new AttributeNotFoundException(e.toString());
405         }
406         catch (InvocationTargetException e)
407         {
408             Log.warn(Log.EXCEPTION, e);
409             throw new ReflectionException((Exception) e.getTargetException());
410         }
411     }
412 
413     /* ------------------------------------------------------------ */
414     public AttributeList setAttributes(AttributeList attrs)
415     {
416         Log.debug("setAttributes");
417 
418         AttributeList results = new AttributeList(attrs.size());
419         Iterator iter = attrs.iterator();
420         while (iter.hasNext())
421         {
422             try
423             {
424                 Attribute attr = (Attribute) iter.next();
425                 setAttribute(attr);
426                 results.add(new Attribute(attr.getName(), getAttribute(attr.getName())));
427             }
428             catch (Exception e)
429             {
430                 Log.warn(Log.EXCEPTION, e);
431             }
432         }
433         return results;
434     }
435 
436     /* ------------------------------------------------------------ */
437     public Object invoke(String name, Object[] params, String[] signature) throws MBeanException, ReflectionException
438     {
439         if (Log.isDebugEnabled())
440             Log.debug("invoke " + name);
441 
442         String methodKey = name + "(";
443         if (signature != null)
444             for (int i = 0; i < signature.length; i++)
445                 methodKey += (i > 0 ? "," : "") + signature[i];
446         methodKey += ")";
447 
448         ClassLoader old_loader=Thread.currentThread().getContextClassLoader();
449         try
450         {
451             Thread.currentThread().setContextClassLoader(_loader);
452             Method method = (Method) _methods.get(methodKey);
453             if (method == null)
454                 throw new NoSuchMethodException(methodKey);
455 
456             Object o = _managed;
457             if (method.getDeclaringClass().isInstance(this))
458                 o = this;
459             return method.invoke(o, params);
460         }
461         catch (NoSuchMethodException e)
462         {
463             Log.warn(Log.EXCEPTION, e);
464             throw new ReflectionException(e);
465         }
466         catch (IllegalAccessException e)
467         {
468             Log.warn(Log.EXCEPTION, e);
469             throw new MBeanException(e);
470         }
471         catch (InvocationTargetException e)
472         {
473             Log.warn(Log.EXCEPTION, e);
474             throw new ReflectionException((Exception) e.getTargetException());
475         }
476         finally
477         {
478             Thread.currentThread().setContextClassLoader(old_loader);
479         }
480     }
481 
482     private static Object findInfluences(Object influences, Class aClass)
483     {
484         if (aClass!=null)
485         {
486             // This class is an influence
487             influences=LazyList.add(influences,aClass);
488 
489             // So are the super classes
490             influences=findInfluences(influences,aClass.getSuperclass());
491 
492             // So are the interfaces
493             Class[] ifs = aClass.getInterfaces();
494             for (int i=0;ifs!=null && i<ifs.length;i++)
495                 influences=findInfluences(influences,ifs[i]);
496         }
497         return influences;
498     }
499 
500     /* ------------------------------------------------------------ */
501     /**
502      * Define an attribute on the managed object. The meta data is defined by looking for standard
503      * getter and setter methods. Descriptions are obtained with a call to findDescription with the
504      * attribute name.
505      *
506      * @param name
507      * @param metaData "description" or "access:description" or "type:access:description"  where type is
508      * one of: <ul>
509      * <li>"Object" The field/method is on the managed object.
510      * <li>"MBean" The field/method is on the mbean proxy object
511      * <li>"MObject" The field/method is on the managed object and value should be converted to MBean reference
512      * <li>"MMBean" The field/method is on the mbean proxy object and value should be converted to MBean reference
513      * </ul>
514      * the access is either "RW" or "RO".
515      */
516     public MBeanAttributeInfo defineAttribute(String name, String metaData)
517     {
518         String description = "";
519         boolean writable = true;
520         boolean onMBean = false;
521         boolean convert = false;
522 
523         if (metaData!= null)
524         {
525             String[] tokens = metaData.split(":", 3);
526             for (int t=0;t<tokens.length-1;t++)
527             {
528                 tokens[t]=tokens[t].trim();
529                 if ("RO".equals(tokens[t]))
530                     writable=false;
531                 else 
532                 {
533                     onMBean=("MMBean".equalsIgnoreCase(tokens[t]) || "MBean".equalsIgnoreCase(tokens[t]));
534                     convert=("MMBean".equalsIgnoreCase(tokens[t]) || "MObject".equalsIgnoreCase(tokens[t]));
535                 }
536             }
537             description=tokens[tokens.length-1];
538         }
539         
540 
541         String uName = name.substring(0, 1).toUpperCase() + name.substring(1);
542         Class oClass = onMBean ? this.getClass() : _managed.getClass();
543 
544         if (Log.isDebugEnabled())
545             Log.debug("defineAttribute "+name+" "+onMBean+":"+writable+":"+oClass+":"+description);
546 
547         Class type = null;
548         Method getter = null;
549         Method setter = null;
550         Method[] methods = oClass.getMethods();
551         for (int m = 0; m < methods.length; m++)
552         {
553             if ((methods[m].getModifiers() & Modifier.PUBLIC) == 0)
554                 continue;
555 
556             // Look for a getter
557             if (methods[m].getName().equals("get" + uName) && methods[m].getParameterTypes().length == 0)
558             {
559                 if (getter != null)
560                     throw new IllegalArgumentException("Multiple getters for attr " + name+ " in "+oClass);
561                 getter = methods[m];
562                 if (type != null && !type.equals(methods[m].getReturnType()))
563                     throw new IllegalArgumentException("Type conflict for attr " + name+ " in "+oClass);
564                 type = methods[m].getReturnType();
565             }
566 
567             // Look for an is getter
568             if (methods[m].getName().equals("is" + uName) && methods[m].getParameterTypes().length == 0)
569             {
570                 if (getter != null)
571                     throw new IllegalArgumentException("Multiple getters for attr " + name+ " in "+oClass);
572                 getter = methods[m];
573                 if (type != null && !type.equals(methods[m].getReturnType()))
574                     throw new IllegalArgumentException("Type conflict for attr " + name+ " in "+oClass);
575                 type = methods[m].getReturnType();
576             }
577 
578             // look for a setter
579             if (writable && methods[m].getName().equals("set" + uName) && methods[m].getParameterTypes().length == 1)
580             {
581                 if (setter != null)
582                     throw new IllegalArgumentException("Multiple setters for attr " + name+ " in "+oClass);
583                 setter = methods[m];
584                 if (type != null && !type.equals(methods[m].getParameterTypes()[0]))
585                     throw new IllegalArgumentException("Type conflict for attr " + name+ " in "+oClass);
586                 type = methods[m].getParameterTypes()[0];
587             }
588         }
589         
590         if (convert && type.isPrimitive() && !type.isArray())
591             throw new IllegalArgumentException("Cannot convert primative " + name);
592 
593 
594         if (getter == null && setter == null)
595             throw new IllegalArgumentException("No getter or setters found for " + name+ " in "+oClass);
596 
597         try
598         {
599             // Remember the methods
600             _getters.put(name, getter);
601             _setters.put(name, setter);
602 
603 
604 
605             MBeanAttributeInfo info=null;
606             if (convert)
607             {
608                 _convert.add(name);
609                 if (type.isArray())
610                     info= new MBeanAttributeInfo(name,OBJECT_NAME_ARRAY_CLASS,description,getter!=null,setter!=null,getter!=null&&getter.getName().startsWith("is"));
611 
612                 else
613                     info= new MBeanAttributeInfo(name,OBJECT_NAME_CLASS,description,getter!=null,setter!=null,getter!=null&&getter.getName().startsWith("is"));
614             }
615             else
616                 info= new MBeanAttributeInfo(name,description,getter,setter);
617 
618             return info;
619         }
620         catch (Exception e)
621         {
622             Log.warn(Log.EXCEPTION, e);
623             throw new IllegalArgumentException(e.toString());
624         }
625     }
626 
627 
628     /* ------------------------------------------------------------ */
629     /**
630      * Define an operation on the managed object. Defines an operation with parameters. Refection is
631      * used to determine find the method and it's return type. The description of the method is
632      * found with a call to findDescription on "name(signature)". The name and description of each
633      * parameter is found with a call to findDescription with "name(signature)[n]", the returned
634      * description is for the last parameter of the partial signature and is assumed to start with
635      * the parameter name, followed by a colon.
636      *
637      * @param metaData "description" or "impact:description" or "type:impact:description", type is
638      * the "Object","MBean", "MMBean" or "MObject" to indicate the method is on the object, the MBean or on the
639      * object but converted to an MBean reference, and impact is either "ACTION","INFO","ACTION_INFO" or "UNKNOWN".
640      */
641     private MBeanOperationInfo defineOperation(String signature, String metaData, ResourceBundle bundle)
642     {
643         String[] tokens=metaData.split(":",3);
644         int i=tokens.length-1;
645         String description=tokens[i--];
646         String impact_name = i<0?"UNKNOWN":tokens[i--].trim();
647         if (i==0)
648             tokens[0]=tokens[0].trim();
649         boolean onMBean= i==0 && ("MBean".equalsIgnoreCase(tokens[0])||"MMBean".equalsIgnoreCase(tokens[0]));
650         boolean convert= i==0 && ("MObject".equalsIgnoreCase(tokens[0])||"MMBean".equalsIgnoreCase(tokens[0]));
651 
652         if (Log.isDebugEnabled())
653             Log.debug("defineOperation "+signature+" "+onMBean+":"+impact_name+":"+description);
654 
655         Class oClass = onMBean ? this.getClass() : _managed.getClass();
656 
657         try
658         {
659             // Resolve the impact
660             int impact=MBeanOperationInfo.UNKNOWN;
661             if (impact_name==null || impact_name.equals("UNKNOWN"))
662                 impact=MBeanOperationInfo.UNKNOWN;
663             else if (impact_name.equals("ACTION"))
664                 impact=MBeanOperationInfo.ACTION;
665             else if (impact_name.equals("INFO"))
666                 impact=MBeanOperationInfo.INFO;
667             else if (impact_name.equals("ACTION_INFO"))
668                 impact=MBeanOperationInfo.ACTION_INFO;
669             else
670                 Log.warn("Unknown impact '"+impact_name+"' for "+signature);
671 
672 
673             // split the signature
674             String[] parts=signature.split("[\\(\\)]");
675             String method_name=parts[0];
676             String arguments=parts.length==2?parts[1]:null;
677             String[] args=arguments==null?new String[0]:arguments.split(" *, *");
678 
679             // Check types and normalize signature.
680             Class[] types = new Class[args.length];
681             MBeanParameterInfo[] pInfo = new MBeanParameterInfo[args.length];
682             signature=method_name;
683             for (i = 0; i < args.length; i++)
684             {
685                 Class type = TypeUtil.fromName(args[i]);
686                 if (type == null)
687                     type = Thread.currentThread().getContextClassLoader().loadClass(args[i]);
688                 types[i] = type;
689                 args[i] = type.isPrimitive() ? TypeUtil.toName(type) : args[i];
690                 signature+=(i>0?",":"(")+args[i];
691             }
692             signature+=(i>0?")":"()");
693 
694             // Build param infos
695             for (i = 0; i < args.length; i++)
696             {
697                 String param_desc = bundle.getString(signature + "[" + i + "]");
698                 parts=param_desc.split(" *: *",2);
699                 if (Log.isDebugEnabled())
700                     Log.debug(parts[0]+": "+parts[1]);
701                 pInfo[i] = new MBeanParameterInfo(parts[0].trim(), args[i], parts[1].trim());
702             }
703 
704             // build the operation info
705             Method method = oClass.getMethod(method_name, types);
706             Class returnClass = method.getReturnType();
707             _methods.put(signature, method);
708             if (convert)
709                 _convert.add(signature);
710 
711             return new MBeanOperationInfo(method_name, description, pInfo, returnClass.isPrimitive() ? TypeUtil.toName(returnClass) : (returnClass.getName()), impact);
712         }
713         catch (Exception e)
714         {
715             Log.warn("Operation '"+signature+"'", e);
716             throw new IllegalArgumentException(e.toString());
717         }
718 
719     }
720 
721 }