001: package net.sourceforge.squirrel_sql.client.session.schemainfo;
002:
003: import java.io.Serializable;
004: import java.sql.SQLException;
005: import java.util.ArrayList;
006: import java.util.Arrays;
007: import java.util.Collections;
008: import java.util.Comparator;
009: import java.util.Hashtable;
010: import java.util.Iterator;
011: import java.util.List;
012: import java.util.Map;
013: import java.util.TreeMap;
014: import java.util.concurrent.CopyOnWriteArrayList;
015:
016: import net.sourceforge.squirrel_sql.client.gui.db.SQLAliasSchemaProperties;
017: import net.sourceforge.squirrel_sql.client.gui.db.SchemaLoadInfo;
018: import net.sourceforge.squirrel_sql.client.gui.db.SchemaNameLoadInfo;
019: import net.sourceforge.squirrel_sql.client.gui.db.SchemaTableTypeCombination;
020: import net.sourceforge.squirrel_sql.client.session.ExtendedColumnInfo;
021: import net.sourceforge.squirrel_sql.client.session.ISession;
022: import net.sourceforge.squirrel_sql.client.session.SessionManager;
023: import net.sourceforge.squirrel_sql.fw.sql.IProcedureInfo;
024: import net.sourceforge.squirrel_sql.fw.sql.ITableInfo;
025: import net.sourceforge.squirrel_sql.fw.sql.TableColumnInfo;
026: import net.sourceforge.squirrel_sql.fw.util.Utilities;
027: import net.sourceforge.squirrel_sql.fw.util.log.ILogger;
028: import net.sourceforge.squirrel_sql.fw.util.log.LoggerController;
029:
030: public class SchemaInfoCache implements Serializable {
031: private static final long serialVersionUID = 2L;
032:
033: private static final ILogger s_log = LoggerController
034: .createLogger(SchemaInfoCache.class);
035:
036: private List<String> _catalogs = new ArrayList<String>();
037: private List<String> _schemas = new ArrayList<String>();
038:
039: private TreeMap<CaseInsensitiveString, String> _keywords = new TreeMap<CaseInsensitiveString, String>();
040: private TreeMap<CaseInsensitiveString, String> _dataTypes = new TreeMap<CaseInsensitiveString, String>();
041: private Map<CaseInsensitiveString, String> _functions = Collections
042: .synchronizedMap(new TreeMap<CaseInsensitiveString, String>());
043:
044: /////////////////////////////////////////////////////////////////////////////
045: // Schema dependent data.
046: // Are changed only in this class
047: //
048: private TreeMap<CaseInsensitiveString, String> _internalTableNameTreeMap = new TreeMap<CaseInsensitiveString, String>();
049:
050: private Map<CaseInsensitiveString, String> _tableNames = Collections
051: .synchronizedMap(_internalTableNameTreeMap);
052:
053: /**
054: * This data structure can be accessed by multiple concurrent threads.
055: * Traversal via iterators is fast and cannot encounter interference from
056: * other threads otherwise ConcurrentModificationExceptions may
057: * result (Bug #1752089)
058: *
059: * One other thing: it must maintain the order in which items were inserted
060: * so that traversal yeilds insertion order (Bug 1805954).
061: */
062: private CopyOnWriteArrayList<ITableInfo> _iTableInfos = new CopyOnWriteArrayList<ITableInfo>();
063:
064: private Hashtable<CaseInsensitiveString, List<ITableInfo>> _tableInfosBySimpleName = new Hashtable<CaseInsensitiveString, List<ITableInfo>>();
065:
066: private Map<CaseInsensitiveString, List<ExtendedColumnInfo>> _extendedColumnInfosByTableName = Collections
067: .synchronizedMap(new TreeMap<CaseInsensitiveString, List<ExtendedColumnInfo>>());
068:
069: private Map<CaseInsensitiveString, List<ExtendedColumnInfo>> _extColumnInfosByColumnName = Collections
070: .synchronizedMap(new TreeMap<CaseInsensitiveString, List<ExtendedColumnInfo>>());
071:
072: private Map<CaseInsensitiveString, String> _procedureNames = Collections
073: .synchronizedMap(new TreeMap<CaseInsensitiveString, String>());
074:
075: private Map<IProcedureInfo, IProcedureInfo> _iProcedureInfos = Collections
076: .synchronizedMap(new TreeMap<IProcedureInfo, IProcedureInfo>());
077:
078: private Hashtable<CaseInsensitiveString, List<IProcedureInfo>> _procedureInfosBySimpleName = new Hashtable<CaseInsensitiveString, List<IProcedureInfo>>();
079: //
080: ///////////////////////////////////////////////////////////////////////////
081:
082: private SQLAliasSchemaProperties _schemaPropsCacheIsBasedOn;
083:
084: private transient String[] _viewTableTypesCacheable;
085: private transient String[] _tabelTableTypesCacheable;
086: //private transient String[] availableTypesInDataBase;
087:
088: private transient ISession _session = null;
089:
090: void setSession(ISession session) {
091: _session = session;
092: initTypes();
093: }
094:
095: boolean loadSchemaIndependentMetaData() {
096: return _session.getAlias().getSchemaProperties()
097: .loadSchemaIndependentMetaData(
098: _schemaPropsCacheIsBasedOn);
099: }
100:
101: private SchemaLoadInfo[] getAllSchemaLoadInfos() {
102: SQLAliasSchemaProperties schemaProps = _session.getAlias()
103: .getSchemaProperties();
104: SchemaLoadInfo[] schemaLoadInfos = schemaProps
105: .getSchemaLoadInfos(_schemaPropsCacheIsBasedOn,
106: _tabelTableTypesCacheable,
107: _viewTableTypesCacheable);
108: SessionManager sessionMgr = _session.getApplication()
109: .getSessionManager();
110: boolean allSchemasAllowed = sessionMgr
111: .areAllSchemasAllowed(_session);
112:
113: if (1 == schemaLoadInfos.length
114: && null == schemaLoadInfos[0].schemaName
115: && false == allSchemasAllowed) {
116: if (false == allSchemasAllowed) {
117: String[] allowedSchemas = sessionMgr
118: .getAllowedSchemas(_session);
119:
120: ArrayList<SchemaLoadInfo> ret = new ArrayList<SchemaLoadInfo>();
121:
122: for (int i = 0; i < allowedSchemas.length; i++) {
123: SchemaLoadInfo buf = (SchemaLoadInfo) Utilities
124: .cloneObject(schemaLoadInfos[0], getClass()
125: .getClassLoader());
126: buf.schemaName = allowedSchemas[i];
127:
128: ret.add(buf);
129: }
130: schemaLoadInfos = ret.toArray(new SchemaLoadInfo[ret
131: .size()]);
132: }
133: }
134: return schemaLoadInfos;
135: }
136:
137: SchemaLoadInfo[] getMatchingSchemaLoadInfos(String schemaName) {
138: return getMatchingSchemaLoadInfos(schemaName, null);
139: }
140:
141: SchemaLoadInfo[] getMatchingSchemaLoadInfos(String schemaName,
142: String[] tableTypes) {
143: if (null == schemaName) {
144: return getAllSchemaLoadInfos();
145: }
146:
147: SchemaLoadInfo[] schemaLoadInfos = getAllSchemaLoadInfos();
148: for (int i = 0; i < schemaLoadInfos.length; i++) {
149: if (null == schemaLoadInfos[i].schemaName
150: || schemaLoadInfos[i].schemaName.equals(schemaName)) {
151:
152: // null == schemaLoadInfos[0].schemaName is the case when there are no _schemas specified
153: // schemaLoadInfos.length will then be 1.
154: schemaLoadInfos[i].schemaName = schemaName;
155: if (null != tableTypes) {
156: SchemaLoadInfo buf = (SchemaLoadInfo) Utilities
157: .cloneObject(schemaLoadInfos[i], getClass()
158: .getClassLoader());
159: buf.tableTypes = tableTypes;
160: return new SchemaLoadInfo[] { buf };
161: }
162:
163: return new SchemaLoadInfo[] { schemaLoadInfos[i] };
164: }
165: }
166: throw new IllegalArgumentException("Unknown Schema "
167: + schemaName);
168: }
169:
170: private void initTypes() {
171: ArrayList<String> tableTypeCandidates = new ArrayList<String>();
172: tableTypeCandidates.add("TABLE");
173: tableTypeCandidates.add("SYSTEM TABLE");
174:
175: ArrayList<String> viewTypeCandidates = new ArrayList<String>();
176: viewTypeCandidates.add("VIEW");
177:
178: try {
179: ArrayList<String> availableBuf = new ArrayList<String>();
180: String[] buf = _session.getSQLConnection().getSQLMetaData()
181: .getTableTypes();
182: availableBuf.addAll(Arrays.asList(buf));
183:
184: for (Iterator<String> i = tableTypeCandidates.iterator(); i
185: .hasNext();) {
186: if (false == availableBuf.contains(i.next())) {
187: i.remove();
188: }
189: }
190:
191: for (Iterator<String> i = viewTypeCandidates.iterator(); i
192: .hasNext();) {
193: if (false == availableBuf.contains(i.next())) {
194: i.remove();
195: }
196: }
197: } catch (SQLException e) {
198: s_log.error("Could not get table types", e);
199: }
200:
201: _tabelTableTypesCacheable = tableTypeCandidates
202: .toArray(new String[tableTypeCandidates.size()]);
203: _viewTableTypesCacheable = viewTypeCandidates
204: .toArray(new String[viewTypeCandidates.size()]);
205: }
206:
207: public boolean isCachedTableType(String type) {
208: boolean found = false;
209:
210: for (int i = 0; i < _viewTableTypesCacheable.length; i++) {
211: if (_viewTableTypesCacheable[i].equals(type)) {
212: found = true;
213: break;
214: }
215: }
216:
217: for (int i = 0; i < _tabelTableTypesCacheable.length; i++) {
218: if (_tabelTableTypesCacheable[i].equals(type)) {
219: found = true;
220: break;
221: }
222: }
223:
224: return found;
225: }
226:
227: static boolean containsType(String[] types, String type) {
228: if (null == types) {
229: return true;
230: }
231:
232: for (int i = 0; i < types.length; i++) {
233: if (type.trim().equalsIgnoreCase(types[i])) {
234: return true;
235: }
236: }
237: return false;
238: }
239:
240: /**
241: * Adds the specified array of ITableInfos to the internal list(s), and sorts
242: * the combination.
243: *
244: * @param infos the array of ITableInfos to add.
245: */
246: public void writeToTableCache(ITableInfo[] infos) {
247: for (ITableInfo info : infos) {
248: String tableName = info.getSimpleName();
249: CaseInsensitiveString ciTableName = new CaseInsensitiveString(
250: tableName);
251: _tableNames.put(ciTableName, tableName);
252:
253: List<ITableInfo> aITabInfos = _tableInfosBySimpleName
254: .get(ciTableName);
255: if (null == aITabInfos) {
256: aITabInfos = new ArrayList<ITableInfo>();
257: _tableInfosBySimpleName.put(ciTableName, aITabInfos);
258: }
259: aITabInfos.add(info);
260: }
261: // CopyOnWriteArrayList is unfortunately not sort-able as a List. So this
262: // will throw an UnsupportedOperationException:
263: //
264: // Collections.sort(_iTableInfos, new TableInfoSimpleNameComparator());
265: //
266: // The following is the best approach according to concurrency master
267: // Doug Lea, in this post:
268: // http://osdir.com/ml/java.jsr.166-concurrency/2004-06/msg00001.html
269: //
270: // Here we copy the existing internal array into a new array that
271: // is large enough to hold the original and new elements. Then sort it.
272: // And finally, create a new CopyOnWriteArrayList with the sorted array.
273:
274: /* Now, create an array large enough to hold the original and the new */
275: int currSize = _iTableInfos.size();
276: ITableInfo[] tableArr = _iTableInfos
277: .toArray(new ITableInfo[currSize + infos.length]);
278: /*
279: * Append the new tables to the new array, starting at the end of the
280: * original
281: */
282: for (int i = 0; i < infos.length; i++) {
283: tableArr[currSize + i] = infos[i];
284: }
285:
286: /* Sort it and store in a new CopyOnWriteArrayList */
287: Arrays.sort(tableArr, new TableInfoSimpleNameComparator());
288: _iTableInfos = new CopyOnWriteArrayList<ITableInfo>(tableArr);
289: }
290:
291: /**
292: * Adds a single ITableInfo to the internal list(s) and re-sorts. This
293: * should not be called in a tight loop iterating over a list of ITableInfos.
294: * If the caller is looping over an array of ITableInfo objects, please use
295: * the version that accepts the ITableInfo array instead.
296: *
297: * @param info the ITableInfo to add.
298: */
299: public void writeToTableCache(ITableInfo info) {
300: writeToTableCache(new ITableInfo[] { info });
301: }
302:
303: public void writeToProcedureCache(IProcedureInfo procedure) {
304: String proc = procedure.getSimpleName();
305: if (proc.length() > 0) {
306: CaseInsensitiveString ciProc = new CaseInsensitiveString(
307: proc);
308: _procedureNames.put(ciProc, proc);
309:
310: List<IProcedureInfo> aIProcInfos = _procedureInfosBySimpleName
311: .get(ciProc);
312: if (null == aIProcInfos) {
313: aIProcInfos = new ArrayList<IProcedureInfo>();
314: _procedureInfosBySimpleName.put(ciProc, aIProcInfos);
315: }
316: aIProcInfos.add(procedure);
317: }
318: _iProcedureInfos.put(procedure, procedure);
319: }
320:
321: public void writeColumsToCache(TableColumnInfo[] infos,
322: CaseInsensitiveString simpleTableName) {
323: ArrayList<ExtendedColumnInfo> ecisInTable = new ArrayList<ExtendedColumnInfo>();
324: for (int i = 0; i < infos.length; i++) {
325: ExtendedColumnInfo eci = new ExtendedColumnInfo(infos[i],
326: simpleTableName.toString());
327: ecisInTable.add(eci);
328:
329: CaseInsensitiveString ciColName = new CaseInsensitiveString(
330: eci.getColumnName());
331: List<ExtendedColumnInfo> ecisInColName = _extColumnInfosByColumnName
332: .get(ciColName);
333: if (null == ecisInColName) {
334: ecisInColName = new ArrayList<ExtendedColumnInfo>();
335: _extColumnInfosByColumnName.put(ciColName,
336: ecisInColName);
337: }
338: ecisInColName.add(eci);
339: }
340:
341: // Note: A CaseInsensitiveString can be a mutable string.
342: // In fact it is a mutable string here because this is usually called from
343: // within Syntax coloring which uses a mutable string.
344: CaseInsensitiveString imutableString = new CaseInsensitiveString(
345: simpleTableName.toString());
346: _extendedColumnInfosByTableName
347: .put(imutableString, ecisInTable);
348: }
349:
350: void initialLoadDone() {
351: /**
352: * When _schemaPropsCacheIsBasedOn is null all loading will be done like there was no cache.
353: *
354: * This will make sure loading only heeds the cache during initial loading.
355: *
356: * Any further loading (via Object tree or tool bar) will be treated as a Cache refresh.
357: */
358: _schemaPropsCacheIsBasedOn = null;
359: }
360:
361: void prepareSerialization() {
362: _schemaPropsCacheIsBasedOn = _session.getAlias()
363: .getSchemaProperties();
364:
365: if (false == _schemaPropsCacheIsBasedOn
366: .isCacheSchemaIndependentMetaData()) {
367: clearSchemaIndependentData();
368: }
369:
370: if (SQLAliasSchemaProperties.GLOBAL_STATE_LOAD_ALL_CACHE_NONE == _schemaPropsCacheIsBasedOn
371: .getGlobalState()) {
372: clearAllSchemaDependentData();
373: } else if (SQLAliasSchemaProperties.GLOBAL_STATE_SPECIFY_SCHEMAS == _schemaPropsCacheIsBasedOn
374: .getGlobalState()) {
375: SchemaTableTypeCombination[] tableTypeCombis = _schemaPropsCacheIsBasedOn
376: .getAllSchemaTableTypeCombinationsNotToBeCached(
377: _tabelTableTypesCacheable,
378: _viewTableTypesCacheable);
379:
380: for (int i = 0; i < tableTypeCombis.length; i++) {
381: clearTables(null, tableTypeCombis[i].schemaName, null,
382: tableTypeCombis[i].types);
383: }
384:
385: String[] procedureSchemas = _schemaPropsCacheIsBasedOn
386: .getAllSchemaProceduresNotToBeCached();
387: for (int i = 0; i < procedureSchemas.length; i++) {
388: clearStoredProcedures(null, procedureSchemas[i], null);
389: }
390:
391: }
392:
393: }
394:
395: void clearAll() {
396: clearSchemaIndependentData();
397:
398: clearAllSchemaDependentData();
399:
400: }
401:
402: private void clearAllSchemaDependentData() {
403: _tableNames.clear();
404: synchronized (_iTableInfos) {
405: _iTableInfos.clear();
406: }
407: _tableInfosBySimpleName.clear();
408:
409: _extColumnInfosByColumnName.clear();
410: _extendedColumnInfosByTableName.clear();
411:
412: _procedureNames.clear();
413: _iProcedureInfos.clear();
414: _procedureInfosBySimpleName.clear();
415:
416: _schemas.clear();
417:
418: }
419:
420: private void clearSchemaIndependentData() {
421: _catalogs.clear();
422:
423: _keywords.clear();
424: _dataTypes.clear();
425: _functions.clear();
426: }
427:
428: void clearTables(String catalogName, String schemaName,
429: String simpleName, String[] types) {
430: for (Iterator<ITableInfo> i = _iTableInfos.iterator(); i
431: .hasNext();) {
432: ITableInfo ti = i.next();
433:
434: boolean matches = matchesMetaString(ti.getCatalogName(),
435: catalogName);
436: matches &= matchesMetaString(ti.getSchemaName(), schemaName);
437: matches &= matchesMetaString(ti.getSimpleName(), simpleName);
438:
439: if (null != types) {
440: boolean found = false;
441: for (int j = 0; j < types.length; j++) {
442: if (types[j].equals(ti.getType())) {
443: found = true;
444: break;
445: }
446: }
447:
448: matches &= found;
449: }
450:
451: if (matches) {
452: // CopyOnWriteArrayList has snapshot iterators that don't support
453: // iterator.remove()
454: _iTableInfos.remove(ti);
455:
456: CaseInsensitiveString ciSimpleName = new CaseInsensitiveString(
457: ti.getSimpleName());
458: List<ITableInfo> tableInfos = _tableInfosBySimpleName
459: .get(ciSimpleName);
460: tableInfos.remove(ti);
461: if (0 == tableInfos.size()) {
462: _tableInfosBySimpleName.remove(ciSimpleName);
463: _tableNames.remove(ciSimpleName);
464: }
465:
466: List<ExtendedColumnInfo> ecisInTable = _extendedColumnInfosByTableName
467: .get(ciSimpleName);
468:
469: if (null == ecisInTable) {
470: // Columns have not yet been loaded
471: continue;
472: }
473:
474: for (Iterator<ExtendedColumnInfo> j = ecisInTable
475: .iterator(); j.hasNext();) {
476: ExtendedColumnInfo eci = j.next();
477:
478: String qn1 = ti.getCatalogName() + "."
479: + ti.getSchemaName() + "."
480: + ti.getSimpleName();
481: String qn2 = eci.getCatalog() + "."
482: + eci.getSchema() + "."
483: + eci.getSimpleTableName();
484: if (new CaseInsensitiveString(qn1)
485: .equals(new CaseInsensitiveString(qn2))) {
486: j.remove();
487: }
488:
489: CaseInsensitiveString ciColName = new CaseInsensitiveString(
490: eci.getColumnName());
491: List<ExtendedColumnInfo> ecisInColumn = _extColumnInfosByColumnName
492: .get(ciColName);
493:
494: if (ecisInColumn != null) {
495: ecisInColumn.remove(eci);
496:
497: if (0 == ecisInColumn.size()) {
498: _extColumnInfosByColumnName
499: .remove(ciColName);
500: }
501: } else {
502: if (s_log.isDebugEnabled()) {
503: s_log
504: .debug("clearTables: no entries in "
505: + "_extColumnInfosByColumnName for column - "
506: + ciColName);
507: }
508: }
509: }
510:
511: if (0 == ecisInTable.size()) {
512: _extendedColumnInfosByTableName
513: .remove(ciSimpleName);
514: }
515: }
516: }
517:
518: }
519:
520: void clearStoredProcedures(String catalogName, String schemaName,
521: String simpleName) {
522: for (Iterator<IProcedureInfo> i = _iProcedureInfos.keySet()
523: .iterator(); i.hasNext();) {
524: IProcedureInfo pi = i.next();
525:
526: boolean matches = matchesMetaString(pi.getCatalogName(),
527: catalogName);
528: matches &= matchesMetaString(pi.getSchemaName(), schemaName);
529: matches &= matchesMetaString(pi.getSimpleName(), simpleName);
530:
531: if (matches) {
532: i.remove();
533:
534: CaseInsensitiveString ciSimpleName = new CaseInsensitiveString(
535: pi.getSimpleName());
536: List<IProcedureInfo> procedureInfos = _procedureInfosBySimpleName
537: .get(ciSimpleName);
538: procedureInfos.remove(pi);
539: if (0 == procedureInfos.size()) {
540: _procedureInfosBySimpleName.remove(ciSimpleName);
541: _procedureNames.remove(ciSimpleName);
542: }
543:
544: }
545: }
546: }
547:
548: private boolean matchesMetaString(String s, String toCheck) {
549: if (null == s || null == toCheck) {
550: return true;
551: }
552:
553: return s.equals(toCheck);
554: }
555:
556: SchemaNameLoadInfo getSchemaNameLoadInfo() {
557: return _session.getAlias().getSchemaProperties()
558: .getSchemaNameLoadInfo(_schemaPropsCacheIsBasedOn);
559: }
560:
561: void writeCatalogs(String[] catalogs) {
562: this ._catalogs.clear();
563: this ._catalogs.addAll(Arrays.asList(catalogs));
564: }
565:
566: void writeSchemas(String[] schemasToWrite) {
567: _schemas.clear();
568: _schemas.addAll(Arrays.asList(schemasToWrite));
569: }
570:
571: void writeKeywords(
572: Hashtable<CaseInsensitiveString, String> keywordsBuf) {
573: _keywords.clear();
574: _keywords.putAll(keywordsBuf);
575: }
576:
577: void writeDataTypes(
578: Hashtable<CaseInsensitiveString, String> dataTypesBuf) {
579: _dataTypes.clear();
580: _dataTypes.putAll(dataTypesBuf);
581: }
582:
583: void writeFunctions(
584: Hashtable<CaseInsensitiveString, String> functionsBuf) {
585: _functions.clear();
586: _functions.putAll(functionsBuf);
587: }
588:
589: List<String> getCatalogsForReadOnly() {
590: return _catalogs;
591: }
592:
593: List<String> getSchemasForReadOnly() {
594: return _schemas;
595: }
596:
597: TreeMap<CaseInsensitiveString, String> getKeywordsForReadOnly() {
598: return _keywords;
599: }
600:
601: TreeMap<CaseInsensitiveString, String> getDataTypesForReadOnly() {
602: return _dataTypes;
603: }
604:
605: Map<CaseInsensitiveString, String> getFunctionsForReadOnly() {
606: return _functions;
607: }
608:
609: Map<CaseInsensitiveString, String> getTableNamesForReadOnly() {
610: return _internalTableNameTreeMap;
611: }
612:
613: List<ITableInfo> getITableInfosForReadOnly() {
614: return _iTableInfos;
615: }
616:
617: Hashtable<CaseInsensitiveString, List<ITableInfo>> getTableInfosBySimpleNameForReadOnly() {
618: return _tableInfosBySimpleName;
619: }
620:
621: Map<CaseInsensitiveString, List<ExtendedColumnInfo>> getExtendedColumnInfosByTableNameForReadOnly() {
622: return _extendedColumnInfosByTableName;
623: }
624:
625: Map<CaseInsensitiveString, List<ExtendedColumnInfo>> getExtColumnInfosByColumnNameForReadOnly() {
626: return _extColumnInfosByColumnName;
627: }
628:
629: Map<CaseInsensitiveString, String> getProcedureNamesForReadOnly() {
630: return _procedureNames;
631: }
632:
633: Map<IProcedureInfo, IProcedureInfo> getIProcedureInfosForReadOnly() {
634: return _iProcedureInfos;
635: }
636:
637: /**
638: * A comparator for ITableInfos that compares them using their simple name.
639: * All other data (such as schema) is ignored, since it isn't likely that we
640: * will need to compare tables in multiple schemas/catalogs in the same list.
641: */
642: private class TableInfoSimpleNameComparator implements
643: Comparator<ITableInfo> {
644: public int compare(ITableInfo o1, ITableInfo o2) {
645: return o1.getSimpleName().compareTo(o2.getSimpleName());
646: }
647: }
648:
649: }
|