001: /**
002: * $Id: DSAMEUtils.java,v 1.8 2005/09/21 11:09:50 dg154973 Exp $
003: * Copyright 2002 Sun Microsystems, Inc. Allrights reserved. Use of
004: * this product is subjectto license terms. Federal Acquisitions:
005: * Commercial Software -- Government Users Subject to Standard License
006: * Terms and Conditions.
007: *
008: * Sun, Sun Microsystems, the Sun logo, and Sun ONE are trademarks or
009: * registered trademarks of Sun Microsystems,Inc. in the United States
010: * and other countries.
011: */package com.sun.ssoadapter.config;
012:
013: import java.util.Map;
014: import java.util.Set;
015: import java.util.HashSet;
016: import java.util.HashMap;
017: import java.util.Hashtable;
018: import java.util.Iterator;
019: import java.util.logging.Level;
020: import java.util.logging.LogRecord;
021: import java.util.logging.Logger;
022:
023: import com.sun.identity.sm.AttributeSchema;
024: import com.sun.identity.sm.AttributeSchema.Type;
025:
026: import com.sun.identity.sm.SchemaType;
027: import com.sun.identity.sm.ServiceSchema;
028: import com.sun.identity.sm.SMSException;
029: import com.sun.ssoadapter.SSOAdapterLogger;
030:
031: /**
032: * There should only be one instance of this class per-jvm. This object
033: * is created by DSAMEServiceAppContext with the getInstance(DSAMEconnection)
034: * invocation and the DSAMEServiceUserContext uses the getInstance() to
035: * get the instance created by DSAMEServiceAppContext.
036: *
037: * Since the DSAMEServiceAppContext object is created first for every jvm
038: * (and only once) we can be sure that only a single instance of this object
039: * exists, and we can get away with not synchronizing access to the object.
040: */
041:
042: public class DSAMEUtils implements DSAMEConstants {
043: //private static Debug debug = DSAMEConnection.debug;
044:
045: private String this Class = null;
046:
047: private static Logger logger = SSOAdapterLogger
048: .getLogger("com.sun.portal.ssoadapter.config");
049:
050: //
051: // Single instance of this object.
052: //
053: private static DSAMEUtils dsameUtils = null;
054:
055: //
056: // Store the DSAMEConnection of the ServiceAppContext object
057: //
058: private DSAMEConnection dsameConnection = null;
059:
060: //
061: // store the service schemas for perf
062: //
063: private Hashtable schemaTable = new Hashtable();
064:
065: //
066: // to represent the currently loading service.
067: //
068: private Map currentlyLoading = new HashMap();
069:
070: //
071: // store the name of the service currently getting loaded
072: //
073:
074: private DSAMEUtils(DSAMEConnection conn) {
075: // private constructor - singleton
076: dsameConnection = conn;
077:
078: this Class = getClass().getName();
079: }
080:
081: public static DSAMEUtils getInstance() {
082: return dsameUtils;
083: }
084:
085: /**
086: * To implemenent a single DSAMEUtils object per-jvm return an existing
087: * dsameUtils if it exists. (ignore the DSAMEConnection)
088: *
089: * @param conn The DSAMEConnection, used to get the schema types for
090: * services.
091: *
092: * @return The single DSAMEUtils object.
093: */
094: public static synchronized DSAMEUtils getInstance(
095: DSAMEConnection conn) {
096: if (dsameUtils == null) {
097: dsameUtils = new DSAMEUtils(conn);
098: }
099:
100: return dsameUtils;
101: }
102:
103: /**
104: * Gets the scope of the attribute in the service
105: */
106:
107: public int getAttributeScope(String serviceName, String attrName) {
108: ServiceSchemaMap srvcSchema = getSchemaMap(serviceName);
109:
110: //
111: // if srvcSchema is null then this is an undefined service
112: //
113: int scope = (srvcSchema != null) ? srvcSchema
114: .getScope(attrName) : UNKNOWN_SERVICE;
115:
116: if (logger.isLoggable(Level.FINEST)) {
117: String[] param = new String[3];
118: param[0] = serviceName;
119: param[1] = attrName;
120: param[2] = scopeArray[scope];
121: logger.log(Level.FINEST, "PSSA_CSSC0080", param);
122: }
123:
124: return scope;
125: }
126:
127: /**
128: * Gets the Type of the attribute in the service
129: */
130:
131: public int getAttributeType(String serviceName, String attrName) {
132: ServiceSchemaMap srvcSchema = getSchemaMap(serviceName);
133:
134: //
135: // if srvcSchema is null then this is an undefined service
136: //
137: int type = (srvcSchema != null) ? srvcSchema.getType(attrName)
138: : UNKNOWN_SERVICE;
139:
140: return type;
141: }
142:
143: /**
144: * Gets the SchemaMap corresponding to the service.
145: *
146: * schemaTable is defined Hashtable, so we dont have to synchronize
147: * get & put.
148: *
149: * Tries to get the service from the schemaTable, if it gets a null,
150: * calls loadAttrsMap() to load the schema.
151: *
152: * Known Problem: If the service was undefined, calls loadAttrsMap()
153: * on every invokation.
154: *
155: * ALGORITHM:
156: * 0. Perf: Try to get the map before any synchronization is done.
157: * Since schemaTable is Hashtable, there wont be structural
158: * inconsistencies.
159: * 0.1 Return if found
160: * 1. sync - Try to get the serviceMap from the Hashtable
161: * 2. if found; return
162: * 2.1 else Look if some one is loading our service
163: * 2.1.1 Create an object monitor and put into currentlyLoading Map
164: * 3. if some one is loading our service;
165: * 3.1 Wait for the loading to finish.
166: * 4. else;
167: * 4.1 Load the AttributeMap
168: * 4.2 Put the Map into the table and remove the Monitor
169: * 5. return the schemaMap
170: *
171: * The loading thread puts a monitor in the currentlyLoading Map using
172: * the serviceName as the key and the waiting thread gets the monitor
173: * from the Map using the same key. This way only the threads trying to
174: * get the same service are serialized.
175: */
176:
177: private ServiceSchemaMap getSchemaMap(String serviceName) {
178: ServiceSchemaMap srvcSchema = null;
179:
180: if (serviceName == null)
181: return null;
182:
183: if ((srvcSchema = (ServiceSchemaMap) schemaTable
184: .get(serviceName)) != null) {
185: //
186: // (0): Performance: To overcome the overhead of synchronization
187: //
188: if (logger.isLoggable(Level.FINEST)) {
189: logger.log(Level.FINEST, "PSSA_CSSC0082", serviceName);
190: }
191: return (srvcSchema);
192: }
193:
194: Object loadIndicator = null;
195: Object nowLoadingIndicator = new Object();
196:
197: //
198: // serialized access by all threads here
199: //
200: synchronized (currentlyLoading) {
201: //
202: // (1) Try to get the serviceMap from the Hashtable
203: //
204:
205: srvcSchema = (ServiceSchemaMap) schemaTable
206: .get(serviceName);
207: if (srvcSchema != null) {
208: //
209: // (2) found in cache
210: //
211: if (logger.isLoggable(Level.FINEST)) {
212: logger.log(Level.FINEST, "PSSA_CSSC0083",
213: serviceName);
214: }
215: return (srvcSchema);
216: } else {
217: //
218: // (2.1) Look if some one is loading our service
219: //
220: loadIndicator = (Object) currentlyLoading
221: .get(serviceName);
222: if (loadIndicator == null) {
223: //(2.1.1) - create Monitor
224: currentlyLoading.put(serviceName,
225: nowLoadingIndicator);
226:
227: if (logger.isLoggable(Level.FINEST)) {
228: logger.log(Level.FINEST, "PSSA_CSSC0084",
229: serviceName);
230: }
231: }
232: }
233: }
234:
235: if (loadIndicator != null) {
236: //
237: // All Waiting threads go here.
238: // (3.1) Wait for the loading to finish.
239: //
240: if (logger.isLoggable(Level.FINEST)) {
241: logger.log(Level.FINEST, "PSSA_CSSC0085", serviceName);
242: }
243:
244: synchronized (loadIndicator) {
245: srvcSchema = (ServiceSchemaMap) schemaTable
246: .get(serviceName);
247: }
248:
249: if (logger.isLoggable(Level.FINEST)) {
250: logger.log(Level.FINEST, "PSSA_CSSC0086", serviceName);
251: }
252:
253: } else {
254: //
255: // All loading threads go here. Serialized access by threads
256: // loading/waiting for the same service.
257: //
258: synchronized (nowLoadingIndicator) {
259: //
260: // (4.1) Load Attribute Map and add it to the schemaTable.
261: //
262: try {
263: srvcSchema = loadAttrsMap(serviceName);
264: } catch (SMSException ame) {
265: //
266: // ignore - log to debug. UNKNOWN service
267: //
268: if (logger.isLoggable(Level.SEVERE)) {
269: LogRecord logRecord = new LogRecord(
270: Level.SEVERE, "PSSA_CSSC0087");
271: logRecord
272: .setParameters(new String[] { serviceName });
273: logRecord.setThrown(ame);
274: logRecord.setLoggerName(logger.getName());
275: logger.log(logRecord);
276: }
277: } catch (Exception e) {
278: //
279: // The AMStoreConnection.getschema*() throws all kinds
280: // of Exceptions. This traps all of those and
281: // Unknown Exception == UNKNOWN service.
282: //
283: if (logger.isLoggable(Level.SEVERE)) {
284: LogRecord logRecord = new LogRecord(
285: Level.SEVERE, "PSSA_CSSC0087");
286: logRecord
287: .setParameters(new String[] { serviceName });
288: logRecord.setThrown(e);
289: logRecord.setLoggerName(logger.getName());
290: logger.log(logRecord);
291: }
292: }
293:
294: synchronized (currentlyLoading) {
295: //
296: // (4.2) Put the Map into the table and remove the Monitor
297: //
298: if (logger.isLoggable(Level.FINEST)) {
299: logger.log(Level.FINEST, "PSSA_CSSC0088",
300: serviceName);
301: }
302:
303: if (srvcSchema != null) {
304: schemaTable.put(serviceName, srvcSchema); // add it
305: }
306:
307: currentlyLoading.remove(serviceName); // remove Monitor
308: }
309: } // sync newLoadMonitor
310: } // else
311:
312: //
313: // (5) return the schema
314: //
315:
316: return srvcSchema;
317: }
318:
319: /**
320: * @return The serviceSchemaMap.
321: */
322:
323: private ServiceSchemaMap loadAttrsMap(String serviceName)
324: throws SMSException {
325: ServiceSchemaMap newMap = new ServiceSchemaMap(serviceName);
326:
327: if (logger.isLoggable(Level.FINEST)) {
328: logger.log(Level.FINEST, "PSSA_CSSC0089", serviceName);
329: }
330:
331: // Get all the scopes defined for this service
332:
333: Set scopes = dsameConnection.getSchemaTypes(serviceName);
334:
335: if (scopes == null || scopes.size() <= 0) {
336: //
337: // return empty ServiceSchemaMap since there are no attributes
338: // defined
339: //
340: if (logger.isLoggable(Level.FINEST)) {
341: logger.log(Level.FINEST, "PSSA_CSSC0090", serviceName);
342: }
343: return newMap;
344: }
345:
346: Iterator iter = scopes.iterator();
347: while (iter.hasNext()) // for every Scope
348: {
349: SchemaType scope = (SchemaType) iter.next();
350:
351: ServiceSchema schema = dsameConnection.getSchema(
352: serviceName, scope);
353: Set attrSchemas = schema.getAttributeSchemas();
354:
355: if (attrSchemas == null && attrSchemas.size() <= 0) {
356: if (logger.isLoggable(Level.FINEST)) {
357: logger.log(Level.FINEST, "PSSA_CSSC0091",
358: serviceName);
359: }
360: continue;
361: }
362:
363: String scopeStr = scope.toString();
364: Iterator itr = attrSchemas.iterator();
365: int ourScope = scopeToInt(scope);
366:
367: while (itr.hasNext()) // for every attribute of Type
368: {
369: AttributeSchema attrSchema = (AttributeSchema) itr
370: .next();
371:
372: AttrSchema aSchema = new AttrSchema();
373:
374: aSchema.attrName = attrSchema.getName();
375: aSchema.type = typeToInt(attrSchema.getType());
376: aSchema.scope = ourScope;
377:
378: if (logger.isLoggable(Level.FINEST)) {
379: String[] param = { scopeStr, serviceName };
380: logger.log(Level.FINEST, "PSSA_CSSC0092", param);
381: }
382:
383: newMap.add(aSchema.attrName, aSchema); // add to the map
384: }
385:
386: } // for
387:
388: return (newMap);
389: }
390:
391: private int scopeToInt(SchemaType scope) {
392: int ourScope = 0;
393:
394: if (scope.equals(SchemaType.GLOBAL)) {
395: ourScope = GLOBAL;
396: } else if (scope.equals(SchemaType.ORGANIZATION)) {
397: ourScope = ORGANIZATION;
398: } else if (scope.equals(SchemaType.DYNAMIC)) {
399: ourScope = DYNAMIC;
400: } else if (scope.equals(SchemaType.USER)) {
401: ourScope = USER;
402: } else if (scope.equals(SchemaType.POLICY)) {
403: ourScope = POLICY;
404: } else {
405: ourScope = UNKNOWN_SCOPE;
406: }
407:
408: return ourScope;
409: }
410:
411: private int typeToInt(AttributeSchema.Type type) {
412: int retType = 0;
413:
414: if (type.equals(AttributeSchema.Type.SINGLE)) {
415: retType = SINGLE;
416: } else if (type.equals(AttributeSchema.Type.SINGLE_CHOICE)) {
417: retType = SINGLE_CHOICE;
418: } else if (type.equals(AttributeSchema.Type.LIST)) {
419: retType = LIST;
420: } else if (type.equals(AttributeSchema.Type.MULTIPLE_CHOICE)) {
421: retType = MULTIPLE_CHOICE;
422: } else if (type.equals(AttributeSchema.Type.SIGNATURE)) {
423: retType = SIGNATURE;
424: } else if (type.equals(AttributeSchema.Type.VALIDATOR)) {
425: retType = VALIDATOR;
426: } else {
427: retType = UNKNOWN_TYPE;
428: }
429:
430: return retType;
431: }
432:
433: /**
434: * Contains a map of all the attributes of the service
435: */
436:
437: private class ServiceSchemaMap {
438: private HashMap attrsMap = new HashMap();
439: private String serviceName = null;
440:
441: public ServiceSchemaMap(String srvcName) {
442: serviceName = srvcName;
443: }
444:
445: public int getScope(String attrName) {
446: AttrSchema attr = (AttrSchema) attrsMap.get(attrName);
447: int scope = (attr == null) ? UNKNOWN_ATTRIBUTE : attr.scope;
448:
449: return (scope);
450: }
451:
452: public int getType(String attrName) {
453: AttrSchema attr = (AttrSchema) attrsMap.get(attrName);
454: int type = (attr == null) ? UNKNOWN_ATTRIBUTE : attr.type;
455:
456: return (type);
457: }
458:
459: public void add(String attrName, AttrSchema attrSchema) {
460: attrsMap.put(attrName, attrSchema);
461: }
462: }
463:
464: /**
465: * Attribute Schema container
466: */
467: private class AttrSchema {
468: // Scope defined in ClientAwareAppContext & Type in AttributeSchema
469:
470: public String attrName = null;
471:
472: //
473: // The scope here is the DSAME's Global/Org/Dynamic/Instance/User
474: //
475: public int scope;
476:
477: //
478: // The type here is actually the type of the attribute - list,
479: // single, single-choice ..
480: //
481: public int type;
482:
483: public String any; // we dont use this now !
484:
485: // Anything else we need goes here
486:
487: }
488:
489: /**
490: * Utility method to get client-aware values from a list
491: *
492: * @param vals The Values to parse.
493: * @param client The client Type to look for
494: * @param awareMap Indicates if the values were obtained from the
495: * "clientType", "default" or not clientaware. And passes back the
496: * parsed objects in various keys. Typically used to avoid being called
497: * multiple times for the same vals. (used by setAttribute())
498: * @param removeClientInfo If the attributes are client-aware, this method
499: * will try to remove the client-type info from the attrs. Setting this
500: * flag to false, prevents this. (Used by setAttribute() to add in the
501: * values of "other" clients.
502: *
503: * @returns the "client" type specific data if found, else looks for
504: * "default" type data, else returns all. The actual parsed values
505: * can be obtained from awareMap.
506: */
507: public Set getClientValues(Set vals, String client, Map awareMap,
508: boolean removeClientInfo) {
509: Set clientSet = null;
510: Set defaultSet = null;
511: Set otherClientSet = null;
512: Set notAwareSet = null;
513:
514: Set retSet = null;
515:
516: client += SEPARATOR;
517:
518: String newVals[] = new String[1];
519:
520: if (vals != null && vals.size() > 0) {
521: Iterator iter = vals.iterator();
522: while (iter.hasNext()) {
523: String val = (String) iter.next();
524:
525: //
526: // Remove the client info - Lookup order is important
527: // 1. client info
528: // 2. default data
529: // 3. Anyother client-data
530: // 4. everything else
531: //
532: int type = checkClientInfo(client, val, newVals,
533: removeClientInfo);
534:
535: if (type == CONTAINS_CLIENT) {
536: if (clientSet == null) // first time
537: clientSet = new HashSet();
538:
539: clientSet.add(newVals[0]);
540: } else if (type == CONTAINS_DEFAULT) {
541: if (defaultSet == null) // first time
542: defaultSet = new HashSet();
543:
544: defaultSet.add(newVals[0]);
545: } else if (type == CONTAINS_OTHER_CLIENTS) {
546: //
547: // other client-aware strings
548: //
549: if (otherClientSet == null)
550: otherClientSet = new HashSet();
551:
552: otherClientSet.add(newVals[0]);
553: } else {
554: //
555: // not client-aware values
556: //
557: if (notAwareSet == null)
558: notAwareSet = new HashSet();
559:
560: notAwareSet.add(newVals[0]);
561: }
562: }
563: }
564:
565: if (clientSet != null) {
566: retSet = clientSet;
567: } else if (defaultSet != null) {
568: retSet = defaultSet;
569: } else {
570: if (otherClientSet == null) {
571: retSet = vals; // no point in using other client data
572: } else {
573: if (notAwareSet != null) {
574: if (logger.isLoggable(Level.WARNING)) {
575: logger.log(Level.WARNING, "PSSA_CSSC0096");
576: }
577: }
578:
579: retSet = null;
580: }
581: }
582:
583: if (awareMap != null) {
584: // send back the parsed values. OK to put nulls
585: //
586: awareMap.put(ORIG_SET, vals);
587: awareMap.put(DEFAULT_SET, defaultSet);
588: awareMap.put(CLIENT_SET, clientSet);
589: awareMap.put(OTHER_CLIENT_SET, otherClientSet);
590: awareMap.put(NOT_AWARE_SET, notAwareSet);
591: }
592:
593: return retSet;
594: }
595:
596: /**
597: * client = "clientType|"
598: * returns what type of client was found and in the newVals - returns
599: * the val string without the client-info
600: */
601:
602: private int checkClientInfo(String client, String val,
603: String[] newVals, boolean rmClientInfo) {
604: int type = -1;
605: int index = -1;
606:
607: if (logger.isLoggable(Level.FINEST)) {
608: String[] param = { client, val };
609: logger.log(Level.FINEST, "PSSA_CSSC0094", param);
610: }
611:
612: if (val == null) {
613: return type;
614: }
615:
616: val = val.trim();
617:
618: if (val.startsWith(client)) {
619: type = CONTAINS_CLIENT;
620: if (newVals != null) {
621: newVals[0] = (rmClientInfo == true) ? val
622: .substring(client.length()) : val;
623: }
624: } else if (val.startsWith(DEFAULT_TYPE)) {
625: type = CONTAINS_DEFAULT;
626: if (newVals != null) {
627: //
628: // DEFAULT_TYPE is "default|"
629: //
630: newVals[0] = (rmClientInfo == true) ? val
631: .substring(DEFAULT_TYPE.length()) : val;
632: }
633: } else if ((index = val.indexOf(SEPARATOR)) != -1) {
634: type = CONTAINS_OTHER_CLIENTS;
635: if (newVals != null) {
636: newVals[0] = (rmClientInfo == true) ? val
637: .substring(index + 1) : val;
638: }
639: } else {
640: type = NOT_CLIENTAWARE;
641: if (newVals != null) {
642: newVals[0] = val;
643: }
644: }
645:
646: /*
647: if (logger.isLoggable (Level.FINEST)) {
648: String retStr = (newVals[0] != null) ? newVals[0]: val;
649: String [] param = {parseTypeArray[type], retStr};
650: logger.log (Level.FINEST, "PSSA_CSSC0095", param);
651: }
652: */
653:
654: return type;
655: }
656:
657: public String getServiceName(Map map) {
658: String name = (String) map.get(SERVICENAME);
659: if (name == null || name.trim().equals(""))
660: name = null; // useless to us - so send a null back
661:
662: return (name);
663: }
664:
665: public String getClientName(Map map) {
666: String name = (String) map.get(CLIENT_TYPE);
667: if (name == null || name.trim().equals(""))
668: name = null; // useless to us - so send a null back
669:
670: return (name);
671: }
672:
673: /**
674: * Modify Values acroding to this Algorithm:
675: *
676: * 1. If no CA attributes exist and CA flag == true, modify all the non
677: * CA attrs by prefixing them with "default|" and add our new vals as
678: * "clientType|"values
679: * 2. No values exist at all - depending on the force CA flag put in
680: * the new values with or without clientType
681: * 3. If some CA attributes exist, assume forceCA == true & modify the
682: * non CA ones with "default|" prefix and our new vals with
683: * "clientTYpe|". If the directory already had our clientType attrs
684: * overwrite them.
685: */
686: public Set modifyValues(String clientType, Map awareMap, Set vals,
687: boolean forceCA) {
688: Set clientSet = (Set) awareMap.get(CLIENT_SET);
689: Set defaultSet = (Set) awareMap.get(DEFAULT_SET);
690: Set otherClientSet = (Set) awareMap.get(OTHER_CLIENT_SET);
691: Set notAwareSet = (Set) awareMap.get(NOT_AWARE_SET);
692:
693: Set newVals = new HashSet();
694:
695: if ((clientSet == null) && (defaultSet == null)
696: && (otherClientSet == null)) {
697: if (logger.isLoggable(Level.FINEST)) {
698: logger.log(Level.FINEST, "PSSA_CSSC0097");
699: }
700:
701: //
702: // None of the values are client aware
703: //
704: if (notAwareSet != null) {
705: if (logger.isLoggable(Level.FINEST))
706: logger.log(Level.FINEST, "PSSA_CSSC0098");
707:
708: if (forceCA == true) {
709: //
710: // rewrite oldVals with "default|" and add new ones with
711: // "client|".
712: //
713: addAsDefault(newVals, DEFAULT_TYPE, notAwareSet);
714: addWithClientInfo(newVals, clientType, vals);
715: } else {
716: //
717: // overwrite the old data
718: //
719: newVals = vals;
720: }
721: } else {
722: if (logger.isLoggable(Level.FINEST)) {
723: logger.log(Level.FINEST, "PSSA_CSSC0099");
724: }
725:
726: //
727: // No old values
728: //
729:
730: if (forceCA == true) {
731: addWithClientInfo(newVals, clientType, vals);
732: } else {
733: newVals.addAll(vals);
734: }
735: }
736: } else {
737: if (notAwareSet != null) {
738: //
739: // rewrite oldVals with "default|" and add new ones with
740: // "client|". Implicit forceClientAwareness
741: //
742: addAsDefault(newVals, DEFAULT_TYPE, notAwareSet);
743: }
744:
745: if (logger.isLoggable(Level.FINEST)) {
746: logger.log(Level.FINEST, "PSSA_CSSC0100");
747: }
748:
749: if (defaultSet != null)
750: newVals.addAll(defaultSet);
751:
752: if (otherClientSet != null)
753: newVals.addAll(otherClientSet);
754:
755: //
756: // Add the new vals with "client|" info
757: //
758: addWithClientInfo(newVals, clientType, vals);
759: }
760:
761: return newVals;
762: }
763:
764: /**
765: * The DEFAULT_TYPE constant already has the "|"
766: */
767: private Set addAsDefault(Set newSet, String client, Set set) {
768: if (set != null) {
769: Iterator iter = set.iterator();
770: while (iter.hasNext()) {
771: String val = (String) iter.next();
772: val = client + val;
773: newSet.add(val);
774: }
775: }
776:
777: return newSet;
778: }
779:
780: private Set addWithClientInfo(Set newSet, String client, Set vals) {
781: if (vals != null) {
782: Iterator iter = vals.iterator();
783: while (iter.hasNext()) {
784: String val = (String) iter.next();
785: val = client + SEPARATOR + val;
786: newSet.add(val);
787: }
788: }
789:
790: return newSet;
791: }
792: }
|