1 /* |
|
2 * Copyright (c) 2000, 2012, 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. Oracle designates this |
|
8 * particular file as subject to the "Classpath" exception as provided |
|
9 * by Oracle in the LICENSE file that accompanied this code. |
|
10 * |
|
11 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
14 * version 2 for more details (a copy is included in the LICENSE file that |
|
15 * accompanied this code). |
|
16 * |
|
17 * You should have received a copy of the GNU General Public License version |
|
18 * 2 along with this work; if not, write to the Free Software Foundation, |
|
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
20 * |
|
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
22 * or visit www.oracle.com if you need additional information or have any |
|
23 * questions. |
|
24 */ |
|
25 |
|
26 package com.sun.jmx.snmp.agent; |
|
27 |
|
28 |
|
29 // java imports |
|
30 // |
|
31 import java.util.Enumeration; |
|
32 import java.util.Iterator; |
|
33 |
|
34 // jmx imports |
|
35 // |
|
36 import javax.management.AttributeList; |
|
37 import javax.management.Attribute; |
|
38 import javax.management.MBeanException; |
|
39 import javax.management.MBeanServer; |
|
40 import javax.management.ObjectName; |
|
41 import javax.management.ReflectionException; |
|
42 import javax.management.InstanceNotFoundException; |
|
43 import javax.management.InvalidAttributeValueException; |
|
44 import javax.management.InstanceAlreadyExistsException; |
|
45 import javax.management.MBeanRegistrationException; |
|
46 import javax.management.NotCompliantMBeanException; |
|
47 import javax.management.RuntimeOperationsException; |
|
48 import com.sun.jmx.snmp.SnmpOid; |
|
49 import com.sun.jmx.snmp.SnmpValue; |
|
50 import com.sun.jmx.snmp.SnmpVarBind; |
|
51 import com.sun.jmx.snmp.SnmpStatusException; |
|
52 |
|
53 |
|
54 /** |
|
55 * <p> |
|
56 * This class is a utility class that transforms SNMP GET / SET requests |
|
57 * into standard JMX getAttributes() setAttributes() requests. |
|
58 * </p> |
|
59 * |
|
60 * <p> |
|
61 * The transformation relies on the metadata information provided by the |
|
62 * {@link com.sun.jmx.snmp.agent.SnmpGenericMetaServer} object which is |
|
63 * passed as the first parameter to every method. This SnmpGenericMetaServer |
|
64 * object is usually a Metadata object generated by <code>mibgen</code>. |
|
65 * </p> |
|
66 * |
|
67 * <p><b><i> |
|
68 * This class is used internally by mibgen generated metadata objects and |
|
69 * you should never need to use it directly. |
|
70 * </b></i></p> |
|
71 * <p><b>This API is a Sun Microsystems internal API and is subject |
|
72 * to change without notice.</b></p> |
|
73 **/ |
|
74 |
|
75 public class SnmpGenericObjectServer { |
|
76 |
|
77 // ---------------------------------------------------------------------- |
|
78 // |
|
79 // Protected variables |
|
80 // |
|
81 // ---------------------------------------------------------------------- |
|
82 |
|
83 /** |
|
84 * The MBean server through which the MBeans will be accessed. |
|
85 **/ |
|
86 protected final MBeanServer server; |
|
87 |
|
88 // ---------------------------------------------------------------------- |
|
89 // |
|
90 // Constructors |
|
91 // |
|
92 // ---------------------------------------------------------------------- |
|
93 |
|
94 /** |
|
95 * Builds a new SnmpGenericObjectServer. Usually there will be a single |
|
96 * object of this type per MIB. |
|
97 * |
|
98 * @param server The MBeanServer in which the MBean accessed by this |
|
99 * MIB are registered. |
|
100 **/ |
|
101 public SnmpGenericObjectServer(MBeanServer server) { |
|
102 this.server = server; |
|
103 } |
|
104 |
|
105 /** |
|
106 * Execute an SNMP GET request. |
|
107 * |
|
108 * <p> |
|
109 * This method first builds the list of attributes that need to be |
|
110 * retrieved from the MBean and then calls getAttributes() on the |
|
111 * MBean server. Then it updates the SnmpMibSubRequest with the values |
|
112 * retrieved from the MBean. |
|
113 * </p> |
|
114 * |
|
115 * <p> |
|
116 * The SNMP metadata information is obtained through the given |
|
117 * <code>meta</code> object, which usually is an instance of a |
|
118 * <code>mibgen</code> generated class. |
|
119 * </p> |
|
120 * |
|
121 * <p><b><i> |
|
122 * This method is called internally by <code>mibgen</code> generated |
|
123 * objects and you should never need to call it directly. |
|
124 * </i></b></p> |
|
125 * |
|
126 * @param meta The metadata object impacted by the subrequest |
|
127 * @param name The ObjectName of the MBean impacted by this subrequest |
|
128 * @param req The SNMP subrequest to execute on the MBean |
|
129 * @param depth The depth of the SNMP object in the OID tree. |
|
130 * |
|
131 * @exception SnmpStatusException whenever an SNMP exception must be |
|
132 * raised. Raising an exception will abort the request.<br> |
|
133 * Exceptions should never be raised directly, but only by means of |
|
134 * <code> |
|
135 * req.registerGetException(<i>VariableId</i>,<i>SnmpStatusException</i>) |
|
136 * </code> |
|
137 **/ |
|
138 public void get(SnmpGenericMetaServer meta, ObjectName name, |
|
139 SnmpMibSubRequest req, int depth) |
|
140 throws SnmpStatusException { |
|
141 |
|
142 // java.lang.System.out.println(">>>>>>>>> GET " + name); |
|
143 |
|
144 final int size = req.getSize(); |
|
145 final Object data = req.getUserData(); |
|
146 final String[] nameList = new String[size]; |
|
147 final SnmpVarBind[] varList = new SnmpVarBind[size]; |
|
148 final long[] idList = new long[size]; |
|
149 int i = 0; |
|
150 |
|
151 for (Enumeration<SnmpVarBind> e=req.getElements(); e.hasMoreElements();) { |
|
152 final SnmpVarBind var= e.nextElement(); |
|
153 try { |
|
154 final long id = var.oid.getOidArc(depth); |
|
155 nameList[i] = meta.getAttributeName(id); |
|
156 varList[i] = var; |
|
157 idList[i] = id; |
|
158 |
|
159 // Check the access rights according to the MIB. |
|
160 // The MBean might be less restrictive (have a getter |
|
161 // while the MIB defines the variable as AFN) |
|
162 // |
|
163 meta.checkGetAccess(id,data); |
|
164 |
|
165 //java.lang.System.out.println(nameList[i] + " added."); |
|
166 i++; |
|
167 } catch(SnmpStatusException x) { |
|
168 //java.lang.System.out.println("exception for " + nameList[i]); |
|
169 //x.printStackTrace(); |
|
170 req.registerGetException(var,x); |
|
171 } |
|
172 } |
|
173 |
|
174 AttributeList result = null; |
|
175 int errorCode = SnmpStatusException.noSuchInstance; |
|
176 |
|
177 try { |
|
178 result = server.getAttributes(name,nameList); |
|
179 } catch (InstanceNotFoundException f) { |
|
180 //java.lang.System.out.println(name + ": instance not found."); |
|
181 //f.printStackTrace(); |
|
182 result = new AttributeList(); |
|
183 } catch (ReflectionException r) { |
|
184 //java.lang.System.out.println(name + ": reflexion error."); |
|
185 //r.printStackTrace(); |
|
186 result = new AttributeList(); |
|
187 } catch (Exception x) { |
|
188 result = new AttributeList(); |
|
189 } |
|
190 |
|
191 |
|
192 final Iterator<?> it = result.iterator(); |
|
193 |
|
194 for (int j=0; j < i; j++) { |
|
195 if (!it.hasNext()) { |
|
196 //java.lang.System.out.println(name + "variable[" + j + |
|
197 // "] absent"); |
|
198 final SnmpStatusException x = |
|
199 new SnmpStatusException(errorCode); |
|
200 req.registerGetException(varList[j],x); |
|
201 continue; |
|
202 } |
|
203 |
|
204 final Attribute att = (Attribute) it.next(); |
|
205 |
|
206 while ((j < i) && (! nameList[j].equals(att.getName()))) { |
|
207 //java.lang.System.out.println(name + "variable[" +j + |
|
208 // "] not found"); |
|
209 final SnmpStatusException x = |
|
210 new SnmpStatusException(errorCode); |
|
211 req.registerGetException(varList[j],x); |
|
212 j++; |
|
213 } |
|
214 |
|
215 if ( j == i) break; |
|
216 |
|
217 try { |
|
218 varList[j].value = |
|
219 meta.buildSnmpValue(idList[j],att.getValue()); |
|
220 } catch (SnmpStatusException x) { |
|
221 req.registerGetException(varList[j],x); |
|
222 } |
|
223 //java.lang.System.out.println(att.getName() + " retrieved."); |
|
224 } |
|
225 //java.lang.System.out.println(">>>>>>>>> END GET"); |
|
226 } |
|
227 |
|
228 /** |
|
229 * Get the value of an SNMP variable. |
|
230 * |
|
231 * <p><b><i> |
|
232 * You should never need to use this method directly. |
|
233 * </i></b></p> |
|
234 * |
|
235 * @param meta The impacted metadata object |
|
236 * @param name The ObjectName of the impacted MBean |
|
237 * @param id The OID arc identifying the variable we're trying to set. |
|
238 * @param data User contextual data allocated through the |
|
239 * {@link com.sun.jmx.snmp.agent.SnmpUserDataFactory} |
|
240 * |
|
241 * @return The value of the variable. |
|
242 * |
|
243 * @exception SnmpStatusException whenever an SNMP exception must be |
|
244 * raised. Raising an exception will abort the request. <br> |
|
245 * Exceptions should never be raised directly, but only by means of |
|
246 * <code> |
|
247 * req.registerGetException(<i>VariableId</i>,<i>SnmpStatusException</i>) |
|
248 * </code> |
|
249 **/ |
|
250 public SnmpValue get(SnmpGenericMetaServer meta, ObjectName name, |
|
251 long id, Object data) |
|
252 throws SnmpStatusException { |
|
253 final String attname = meta.getAttributeName(id); |
|
254 Object result = null; |
|
255 |
|
256 try { |
|
257 result = server.getAttribute(name,attname); |
|
258 } catch (MBeanException m) { |
|
259 Exception t = m.getTargetException(); |
|
260 if (t instanceof SnmpStatusException) |
|
261 throw (SnmpStatusException) t; |
|
262 throw new SnmpStatusException(SnmpStatusException.noSuchInstance); |
|
263 } catch (Exception e) { |
|
264 throw new SnmpStatusException(SnmpStatusException.noSuchInstance); |
|
265 } |
|
266 |
|
267 return meta.buildSnmpValue(id,result); |
|
268 } |
|
269 |
|
270 /** |
|
271 * Execute an SNMP SET request. |
|
272 * |
|
273 * <p> |
|
274 * This method first builds the list of attributes that need to be |
|
275 * set on the MBean and then calls setAttributes() on the |
|
276 * MBean server. Then it updates the SnmpMibSubRequest with the new |
|
277 * values retrieved from the MBean. |
|
278 * </p> |
|
279 * |
|
280 * <p> |
|
281 * The SNMP metadata information is obtained through the given |
|
282 * <code>meta</code> object, which usually is an instance of a |
|
283 * <code>mibgen</code> generated class. |
|
284 * </p> |
|
285 * |
|
286 * <p><b><i> |
|
287 * This method is called internally by <code>mibgen</code> generated |
|
288 * objects and you should never need to call it directly. |
|
289 * </i></b></p> |
|
290 * |
|
291 * @param meta The metadata object impacted by the subrequest |
|
292 * @param name The ObjectName of the MBean impacted by this subrequest |
|
293 * @param req The SNMP subrequest to execute on the MBean |
|
294 * @param depth The depth of the SNMP object in the OID tree. |
|
295 * |
|
296 * @exception SnmpStatusException whenever an SNMP exception must be |
|
297 * raised. Raising an exception will abort the request. <br> |
|
298 * Exceptions should never be raised directly, but only by means of |
|
299 * <code> |
|
300 * req.registerGetException(<i>VariableId</i>,<i>SnmpStatusException</i>) |
|
301 * </code> |
|
302 **/ |
|
303 public void set(SnmpGenericMetaServer meta, ObjectName name, |
|
304 SnmpMibSubRequest req, int depth) |
|
305 throws SnmpStatusException { |
|
306 |
|
307 final int size = req.getSize(); |
|
308 final AttributeList attList = new AttributeList(size); |
|
309 final String[] nameList = new String[size]; |
|
310 final SnmpVarBind[] varList = new SnmpVarBind[size]; |
|
311 final long[] idList = new long[size]; |
|
312 int i = 0; |
|
313 |
|
314 for (Enumeration<SnmpVarBind> e=req.getElements(); e.hasMoreElements();) { |
|
315 final SnmpVarBind var= e.nextElement(); |
|
316 try { |
|
317 final long id = var.oid.getOidArc(depth); |
|
318 final String attname = meta.getAttributeName(id); |
|
319 final Object attvalue= |
|
320 meta.buildAttributeValue(id,var.value); |
|
321 final Attribute att = new Attribute(attname,attvalue); |
|
322 attList.add(att); |
|
323 nameList[i] = attname; |
|
324 varList[i] = var; |
|
325 idList[i] = id; |
|
326 i++; |
|
327 } catch(SnmpStatusException x) { |
|
328 req.registerSetException(var,x); |
|
329 } |
|
330 } |
|
331 |
|
332 AttributeList result; |
|
333 int errorCode = SnmpStatusException.noAccess; |
|
334 |
|
335 try { |
|
336 result = server.setAttributes(name,attList); |
|
337 } catch (InstanceNotFoundException f) { |
|
338 result = new AttributeList(); |
|
339 errorCode = SnmpStatusException.snmpRspInconsistentName; |
|
340 } catch (ReflectionException r) { |
|
341 errorCode = SnmpStatusException.snmpRspInconsistentName; |
|
342 result = new AttributeList(); |
|
343 } catch (Exception x) { |
|
344 result = new AttributeList(); |
|
345 } |
|
346 |
|
347 final Iterator<?> it = result.iterator(); |
|
348 |
|
349 for (int j=0; j < i; j++) { |
|
350 if (!it.hasNext()) { |
|
351 final SnmpStatusException x = |
|
352 new SnmpStatusException(errorCode); |
|
353 req.registerSetException(varList[j],x); |
|
354 continue; |
|
355 } |
|
356 |
|
357 final Attribute att = (Attribute) it.next(); |
|
358 |
|
359 while ((j < i) && (! nameList[j].equals(att.getName()))) { |
|
360 final SnmpStatusException x = |
|
361 new SnmpStatusException(SnmpStatusException.noAccess); |
|
362 req.registerSetException(varList[j],x); |
|
363 j++; |
|
364 } |
|
365 |
|
366 if ( j == i) break; |
|
367 |
|
368 try { |
|
369 varList[j].value = |
|
370 meta.buildSnmpValue(idList[j],att.getValue()); |
|
371 } catch (SnmpStatusException x) { |
|
372 req.registerSetException(varList[j],x); |
|
373 } |
|
374 |
|
375 } |
|
376 } |
|
377 |
|
378 /** |
|
379 * Set the value of an SNMP variable. |
|
380 * |
|
381 * <p><b><i> |
|
382 * You should never need to use this method directly. |
|
383 * </i></b></p> |
|
384 * |
|
385 * @param meta The impacted metadata object |
|
386 * @param name The ObjectName of the impacted MBean |
|
387 * @param x The new requested SnmpValue |
|
388 * @param id The OID arc identifying the variable we're trying to set. |
|
389 * @param data User contextual data allocated through the |
|
390 * {@link com.sun.jmx.snmp.agent.SnmpUserDataFactory} |
|
391 * |
|
392 * @return The new value of the variable after the operation. |
|
393 * |
|
394 * @exception SnmpStatusException whenever an SNMP exception must be |
|
395 * raised. Raising an exception will abort the request. <br> |
|
396 * Exceptions should never be raised directly, but only by means of |
|
397 * <code> |
|
398 * req.registerSetException(<i>VariableId</i>,<i>SnmpStatusException</i>) |
|
399 * </code> |
|
400 **/ |
|
401 public SnmpValue set(SnmpGenericMetaServer meta, ObjectName name, |
|
402 SnmpValue x, long id, Object data) |
|
403 throws SnmpStatusException { |
|
404 final String attname = meta.getAttributeName(id); |
|
405 final Object attvalue= |
|
406 meta.buildAttributeValue(id,x); |
|
407 final Attribute att = new Attribute(attname,attvalue); |
|
408 |
|
409 Object result = null; |
|
410 |
|
411 try { |
|
412 server.setAttribute(name,att); |
|
413 result = server.getAttribute(name,attname); |
|
414 } catch(InvalidAttributeValueException iv) { |
|
415 throw new |
|
416 SnmpStatusException(SnmpStatusException.snmpRspWrongValue); |
|
417 } catch (InstanceNotFoundException f) { |
|
418 throw new |
|
419 SnmpStatusException(SnmpStatusException.snmpRspInconsistentName); |
|
420 } catch (ReflectionException r) { |
|
421 throw new |
|
422 SnmpStatusException(SnmpStatusException.snmpRspInconsistentName); |
|
423 } catch (MBeanException m) { |
|
424 Exception t = m.getTargetException(); |
|
425 if (t instanceof SnmpStatusException) |
|
426 throw (SnmpStatusException) t; |
|
427 throw new |
|
428 SnmpStatusException(SnmpStatusException.noAccess); |
|
429 } catch (Exception e) { |
|
430 throw new |
|
431 SnmpStatusException(SnmpStatusException.noAccess); |
|
432 } |
|
433 |
|
434 return meta.buildSnmpValue(id,result); |
|
435 } |
|
436 |
|
437 /** |
|
438 * Checks whether an SNMP SET request can be successfully performed. |
|
439 * |
|
440 * <p> |
|
441 * For each variable in the subrequest, this method calls |
|
442 * checkSetAccess() on the meta object, and then tries to invoke the |
|
443 * check<i>AttributeName</i>() method on the MBean. If this method |
|
444 * is not defined then it is assumed that the SET won't fail. |
|
445 * </p> |
|
446 * |
|
447 * <p><b><i> |
|
448 * This method is called internally by <code>mibgen</code> generated |
|
449 * objects and you should never need to call it directly. |
|
450 * </i></b></p> |
|
451 * |
|
452 * @param meta The metadata object impacted by the subrequest |
|
453 * @param name The ObjectName of the MBean impacted by this subrequest |
|
454 * @param req The SNMP subrequest to execute on the MBean |
|
455 * @param depth The depth of the SNMP object in the OID tree. |
|
456 * |
|
457 * @exception SnmpStatusException if the requested SET operation must |
|
458 * be rejected. Raising an exception will abort the request. <br> |
|
459 * Exceptions should never be raised directly, but only by means of |
|
460 * <code> |
|
461 * req.registerCheckException(<i>VariableId</i>,<i>SnmpStatusException</i>) |
|
462 * </code> |
|
463 * |
|
464 **/ |
|
465 public void check(SnmpGenericMetaServer meta, ObjectName name, |
|
466 SnmpMibSubRequest req, int depth) |
|
467 throws SnmpStatusException { |
|
468 |
|
469 final Object data = req.getUserData(); |
|
470 |
|
471 for (Enumeration<SnmpVarBind> e=req.getElements(); e.hasMoreElements();) { |
|
472 final SnmpVarBind var= e.nextElement(); |
|
473 try { |
|
474 final long id = var.oid.getOidArc(depth); |
|
475 // call meta.check() here, and meta.check will call check() |
|
476 check(meta,name,var.value,id,data); |
|
477 } catch(SnmpStatusException x) { |
|
478 req.registerCheckException(var,x); |
|
479 } |
|
480 } |
|
481 } |
|
482 |
|
483 /** |
|
484 * Checks whether a SET operation can be performed on a given SNMP |
|
485 * variable. |
|
486 * |
|
487 * @param meta The impacted metadata object |
|
488 * @param name The ObjectName of the impacted MBean |
|
489 * @param x The new requested SnmpValue |
|
490 * @param id The OID arc identifying the variable we're trying to set. |
|
491 * @param data User contextual data allocated through the |
|
492 * {@link com.sun.jmx.snmp.agent.SnmpUserDataFactory} |
|
493 * |
|
494 * <p> |
|
495 * This method calls checkSetAccess() on the meta object, and then |
|
496 * tries to invoke the check<i>AttributeName</i>() method on the MBean. |
|
497 * If this method is not defined then it is assumed that the SET |
|
498 * won't fail. |
|
499 * </p> |
|
500 * |
|
501 * <p><b><i> |
|
502 * This method is called internally by <code>mibgen</code> generated |
|
503 * objects and you should never need to call it directly. |
|
504 * </i></b></p> |
|
505 * |
|
506 * @exception SnmpStatusException if the requested SET operation must |
|
507 * be rejected. Raising an exception will abort the request. <br> |
|
508 * Exceptions should never be raised directly, but only by means of |
|
509 * <code> |
|
510 * req.registerCheckException(<i>VariableId</i>,<i>SnmpStatusException</i>) |
|
511 * </code> |
|
512 * |
|
513 **/ |
|
514 // XXX xxx ZZZ zzz Maybe we should go through the MBeanInfo here? |
|
515 public void check(SnmpGenericMetaServer meta, ObjectName name, |
|
516 SnmpValue x, long id, Object data) |
|
517 throws SnmpStatusException { |
|
518 |
|
519 meta.checkSetAccess(x,id,data); |
|
520 try { |
|
521 final String attname = meta.getAttributeName(id); |
|
522 final Object attvalue= meta.buildAttributeValue(id,x); |
|
523 final Object[] params = new Object[1]; |
|
524 final String[] signature = new String[1]; |
|
525 |
|
526 params[0] = attvalue; |
|
527 signature[0] = attvalue.getClass().getName(); |
|
528 server.invoke(name,"check"+attname,params,signature); |
|
529 |
|
530 } catch( SnmpStatusException e) { |
|
531 throw e; |
|
532 } |
|
533 catch (InstanceNotFoundException i) { |
|
534 throw new |
|
535 SnmpStatusException(SnmpStatusException.snmpRspInconsistentName); |
|
536 } catch (ReflectionException r) { |
|
537 // checkXXXX() not defined => do nothing |
|
538 } catch (MBeanException m) { |
|
539 Exception t = m.getTargetException(); |
|
540 if (t instanceof SnmpStatusException) |
|
541 throw (SnmpStatusException) t; |
|
542 throw new SnmpStatusException(SnmpStatusException.noAccess); |
|
543 } catch (Exception e) { |
|
544 throw new |
|
545 SnmpStatusException(SnmpStatusException.noAccess); |
|
546 } |
|
547 } |
|
548 |
|
549 public void registerTableEntry(SnmpMibTable meta, SnmpOid rowOid, |
|
550 ObjectName objname, Object entry) |
|
551 throws SnmpStatusException { |
|
552 if (objname == null) |
|
553 throw new |
|
554 SnmpStatusException(SnmpStatusException.snmpRspInconsistentName); |
|
555 try { |
|
556 if (entry != null && !server.isRegistered(objname)) |
|
557 server.registerMBean(entry, objname); |
|
558 } catch (InstanceAlreadyExistsException e) { |
|
559 throw new |
|
560 SnmpStatusException(SnmpStatusException.snmpRspInconsistentName); |
|
561 } catch (MBeanRegistrationException e) { |
|
562 throw new SnmpStatusException(SnmpStatusException.snmpRspNoAccess); |
|
563 } catch (NotCompliantMBeanException e) { |
|
564 throw new SnmpStatusException(SnmpStatusException.snmpRspGenErr); |
|
565 } catch (RuntimeOperationsException e) { |
|
566 throw new SnmpStatusException(SnmpStatusException.snmpRspGenErr); |
|
567 } catch(Exception e) { |
|
568 throw new SnmpStatusException(SnmpStatusException.snmpRspGenErr); |
|
569 } |
|
570 } |
|
571 |
|
572 } |
|