View Javadoc

1   /**
2    * Copyright 2009 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  package org.apache.hadoop.hbase;
21  
22  import java.io.DataInput;
23  import java.io.DataOutput;
24  import java.io.IOException;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.TreeMap;
32  
33  import org.apache.hadoop.fs.Path;
34  import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
35  import org.apache.hadoop.hbase.io.hfile.Compression;
36  import org.apache.hadoop.hbase.regionserver.StoreFile;
37  import org.apache.hadoop.hbase.util.Bytes;
38  import org.apache.hadoop.io.WritableComparable;
39  
40  /**
41   * HTableDescriptor contains the name of an HTable, and its
42   * column families.
43   */
44  public class HTableDescriptor implements WritableComparable<HTableDescriptor> {
45  
46    // Changes prior to version 3 were not recorded here.
47    // Version 3 adds metadata as a map where keys and values are byte[].
48    // Version 4 adds indexes
49    // Version 5 removed transactional pollution -- e.g. indexes
50    public static final byte TABLE_DESCRIPTOR_VERSION = 5;
51  
52    private byte [] name = HConstants.EMPTY_BYTE_ARRAY;
53    private String nameAsString = "";
54  
55    // Table metadata
56    protected Map<ImmutableBytesWritable, ImmutableBytesWritable> values =
57      new HashMap<ImmutableBytesWritable, ImmutableBytesWritable>();
58  
59    public static final String FAMILIES = "FAMILIES";
60    public static final ImmutableBytesWritable FAMILIES_KEY =
61      new ImmutableBytesWritable(Bytes.toBytes(FAMILIES));
62    public static final String MAX_FILESIZE = "MAX_FILESIZE";
63    public static final ImmutableBytesWritable MAX_FILESIZE_KEY =
64      new ImmutableBytesWritable(Bytes.toBytes(MAX_FILESIZE));
65    public static final String READONLY = "READONLY";
66    public static final ImmutableBytesWritable READONLY_KEY =
67      new ImmutableBytesWritable(Bytes.toBytes(READONLY));
68    public static final String MEMSTORE_FLUSHSIZE = "MEMSTORE_FLUSHSIZE";
69    public static final ImmutableBytesWritable MEMSTORE_FLUSHSIZE_KEY =
70      new ImmutableBytesWritable(Bytes.toBytes(MEMSTORE_FLUSHSIZE));
71    public static final String IS_ROOT = "IS_ROOT";
72    public static final ImmutableBytesWritable IS_ROOT_KEY =
73      new ImmutableBytesWritable(Bytes.toBytes(IS_ROOT));
74    public static final String IS_META = "IS_META";
75  
76    public static final ImmutableBytesWritable IS_META_KEY =
77      new ImmutableBytesWritable(Bytes.toBytes(IS_META));
78  
79    public static final String DEFERRED_LOG_FLUSH = "DEFERRED_LOG_FLUSH";
80    public static final ImmutableBytesWritable DEFERRED_LOG_FLUSH_KEY =
81      new ImmutableBytesWritable(Bytes.toBytes(DEFERRED_LOG_FLUSH));
82  
83  
84    // The below are ugly but better than creating them each time till we
85    // replace booleans being saved as Strings with plain booleans.  Need a
86    // migration script to do this.  TODO.
87    private static final ImmutableBytesWritable FALSE =
88      new ImmutableBytesWritable(Bytes.toBytes(Boolean.FALSE.toString()));
89    private static final ImmutableBytesWritable TRUE =
90      new ImmutableBytesWritable(Bytes.toBytes(Boolean.TRUE.toString()));
91  
92    public static final boolean DEFAULT_READONLY = false;
93  
94    public static final long DEFAULT_MEMSTORE_FLUSH_SIZE = 1024*1024*64L;
95  
96    public static final long DEFAULT_MAX_FILESIZE = 1024*1024*256L;
97  
98    public static final boolean DEFAULT_DEFERRED_LOG_FLUSH = false;
99  
100   private volatile Boolean meta = null;
101   private volatile Boolean root = null;
102   private Boolean isDeferredLog = null;
103 
104   // Key is hash of the family name.
105   public final Map<byte [], HColumnDescriptor> families =
106     new TreeMap<byte [], HColumnDescriptor>(Bytes.BYTES_RAWCOMPARATOR);
107 
108   /**
109    * Private constructor used internally creating table descriptors for
110    * catalog tables: e.g. .META. and -ROOT-.
111    */
112   protected HTableDescriptor(final byte [] name, HColumnDescriptor[] families) {
113     this.name = name.clone();
114     this.nameAsString = Bytes.toString(this.name);
115     setMetaFlags(name);
116     for(HColumnDescriptor descriptor : families) {
117       this.families.put(descriptor.getName(), descriptor);
118     }
119   }
120 
121   /**
122    * Private constructor used internally creating table descriptors for
123    * catalog tables: e.g. .META. and -ROOT-.
124    */
125   protected HTableDescriptor(final byte [] name, HColumnDescriptor[] families,
126       Map<ImmutableBytesWritable,ImmutableBytesWritable> values) {
127     this.name = name.clone();
128     this.nameAsString = Bytes.toString(this.name);
129     setMetaFlags(name);
130     for(HColumnDescriptor descriptor : families) {
131       this.families.put(descriptor.getName(), descriptor);
132     }
133     for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> entry:
134         values.entrySet()) {
135       this.values.put(entry.getKey(), entry.getValue());
136     }
137   }
138 
139 
140   /**
141    * Constructs an empty object.
142    * For deserializing an HTableDescriptor instance only.
143    * @see #HTableDescriptor(byte[])
144    */
145   public HTableDescriptor() {
146     super();
147   }
148 
149   /**
150    * Constructor.
151    * @param name Table name.
152    * @throws IllegalArgumentException if passed a table name
153    * that is made of other than 'word' characters, underscore or period: i.e.
154    * <code>[a-zA-Z_0-9.].
155    * @see <a href="HADOOP-1581">HADOOP-1581 HBASE: Un-openable tablename bug</a>
156    */
157   public HTableDescriptor(final String name) {
158     this(Bytes.toBytes(name));
159   }
160 
161   /**
162    * Constructor.
163    * @param name Table name.
164    * @throws IllegalArgumentException if passed a table name
165    * that is made of other than 'word' characters, underscore or period: i.e.
166    * <code>[a-zA-Z_0-9-.].
167    * @see <a href="HADOOP-1581">HADOOP-1581 HBASE: Un-openable tablename bug</a>
168    */
169   public HTableDescriptor(final byte [] name) {
170     super();
171     setMetaFlags(this.name);
172     this.name = this.isMetaRegion()? name: isLegalTableName(name);
173     this.nameAsString = Bytes.toString(this.name);
174   }
175 
176   /**
177    * Constructor.
178    * <p>
179    * Makes a deep copy of the supplied descriptor.
180    * Can make a modifiable descriptor from an UnmodifyableHTableDescriptor.
181    * @param desc The descriptor.
182    */
183   public HTableDescriptor(final HTableDescriptor desc) {
184     super();
185     this.name = desc.name.clone();
186     this.nameAsString = Bytes.toString(this.name);
187     setMetaFlags(this.name);
188     for (HColumnDescriptor c: desc.families.values()) {
189       this.families.put(c.getName(), new HColumnDescriptor(c));
190     }
191     for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e:
192         desc.values.entrySet()) {
193       this.values.put(e.getKey(), e.getValue());
194     }
195   }
196 
197   /*
198    * Set meta flags on this table.
199    * Called by constructors.
200    * @param name
201    */
202   private void setMetaFlags(final byte [] name) {
203     setRootRegion(Bytes.equals(name, HConstants.ROOT_TABLE_NAME));
204     setMetaRegion(isRootRegion() ||
205       Bytes.equals(name, HConstants.META_TABLE_NAME));
206   }
207 
208   /** @return true if this is the root region */
209   public boolean isRootRegion() {
210     if (this.root == null) {
211       this.root = isSomething(IS_ROOT_KEY, false)? Boolean.TRUE: Boolean.FALSE;
212     }
213     return this.root.booleanValue();
214   }
215 
216   /** @param isRoot true if this is the root region */
217   protected void setRootRegion(boolean isRoot) {
218     // TODO: Make the value a boolean rather than String of boolean.
219     values.put(IS_ROOT_KEY, isRoot? TRUE: FALSE);
220   }
221 
222   /** @return true if this is a meta region (part of the root or meta tables) */
223   public boolean isMetaRegion() {
224     if (this.meta == null) {
225       this.meta = calculateIsMetaRegion();
226     }
227     return this.meta.booleanValue();
228   }
229 
230   private synchronized Boolean calculateIsMetaRegion() {
231     byte [] value = getValue(IS_META_KEY);
232     return (value != null)? Boolean.valueOf(Bytes.toString(value)): Boolean.FALSE;
233   }
234 
235   private boolean isSomething(final ImmutableBytesWritable key,
236       final boolean valueIfNull) {
237     byte [] value = getValue(key);
238     if (value != null) {
239       // TODO: Make value be a boolean rather than String of boolean.
240       return Boolean.valueOf(Bytes.toString(value)).booleanValue();
241     }
242     return valueIfNull;
243   }
244 
245   /**
246    * @param isMeta true if this is a meta region (part of the root or meta
247    * tables) */
248   protected void setMetaRegion(boolean isMeta) {
249     values.put(IS_META_KEY, isMeta? TRUE: FALSE);
250   }
251 
252   /** @return true if table is the meta table */
253   public boolean isMetaTable() {
254     return isMetaRegion() && !isRootRegion();
255   }
256 
257   /**
258    * @param n Table name.
259    * @return True if a catalog table, -ROOT- or .META.
260    */
261   public static boolean isMetaTable(final byte [] n) {
262     return Bytes.equals(n, HConstants.ROOT_TABLE_NAME) ||
263       Bytes.equals(n, HConstants.META_TABLE_NAME);
264   }
265 
266   /**
267    * Check passed buffer is legal user-space table name.
268    * @param b Table name.
269    * @return Returns passed <code>b</code> param
270    * @throws NullPointerException If passed <code>b</code> is null
271    * @throws IllegalArgumentException if passed a table name
272    * that is made of other than 'word' characters or underscores: i.e.
273    * <code>[a-zA-Z_0-9].
274    */
275   public static byte [] isLegalTableName(final byte [] b) {
276     if (b == null || b.length <= 0) {
277       throw new IllegalArgumentException("Name is null or empty");
278     }
279     if (b[0] == '.' || b[0] == '-') {
280       throw new IllegalArgumentException("Illegal first character <" + b[0] +
281           "> at 0. User-space table names can only start with 'word " +
282           "characters': i.e. [a-zA-Z_0-9]: " + Bytes.toString(b));
283     }
284     for (int i = 0; i < b.length; i++) {
285       if (Character.isLetterOrDigit(b[i]) || b[i] == '_' || b[i] == '-' ||
286           b[i] == '.') {
287         continue;
288       }
289       throw new IllegalArgumentException("Illegal character <" + b[i] +
290         "> at " + i + ". User-space table names can only contain " +
291         "'word characters': i.e. [a-zA-Z_0-9-.]: " + Bytes.toString(b));
292     }
293     return b;
294   }
295 
296   /**
297    * @param key The key.
298    * @return The value.
299    */
300   public byte[] getValue(byte[] key) {
301     return getValue(new ImmutableBytesWritable(key));
302   }
303 
304   private byte[] getValue(final ImmutableBytesWritable key) {
305     ImmutableBytesWritable ibw = values.get(key);
306     if (ibw == null)
307       return null;
308     return ibw.get();
309   }
310 
311   /**
312    * @param key The key.
313    * @return The value as a string.
314    */
315   public String getValue(String key) {
316     byte[] value = getValue(Bytes.toBytes(key));
317     if (value == null)
318       return null;
319     return Bytes.toString(value);
320   }
321 
322   /**
323    * @return All values.
324    */
325   public Map<ImmutableBytesWritable,ImmutableBytesWritable> getValues() {
326      return Collections.unmodifiableMap(values);
327   }
328 
329   /**
330    * @param key The key.
331    * @param value The value.
332    */
333   public void setValue(byte[] key, byte[] value) {
334     setValue(new ImmutableBytesWritable(key), value);
335   }
336 
337   /*
338    * @param key The key.
339    * @param value The value.
340    */
341   private void setValue(final ImmutableBytesWritable key,
342       final byte[] value) {
343     values.put(key, new ImmutableBytesWritable(value));
344   }
345 
346   /*
347    * @param key The key.
348    * @param value The value.
349    */
350   private void setValue(final ImmutableBytesWritable key,
351       final ImmutableBytesWritable value) {
352     values.put(key, value);
353   }
354 
355   /**
356    * @param key The key.
357    * @param value The value.
358    */
359   public void setValue(String key, String value) {
360     setValue(Bytes.toBytes(key), Bytes.toBytes(value));
361   }
362 
363   /**
364    * @param key Key whose key and value we're to remove from HTD parameters.
365    */
366   public void remove(final byte [] key) {
367     values.remove(new ImmutableBytesWritable(key));
368   }
369 
370   /**
371    * @return true if all columns in the table should be read only
372    */
373   public boolean isReadOnly() {
374     return isSomething(READONLY_KEY, DEFAULT_READONLY);
375   }
376 
377   /**
378    * @param readOnly True if all of the columns in the table should be read
379    * only.
380    */
381   public void setReadOnly(final boolean readOnly) {
382     setValue(READONLY_KEY, readOnly? TRUE: FALSE);
383   }
384 
385   /**
386    * @return true if that table's log is hflush by other means
387    */
388   public synchronized boolean isDeferredLogFlush() {
389     if(this.isDeferredLog == null) {
390       this.isDeferredLog =
391           isSomething(DEFERRED_LOG_FLUSH_KEY, DEFAULT_DEFERRED_LOG_FLUSH);
392     }
393     return this.isDeferredLog;
394   }
395 
396   /**
397    * @param isDeferredLogFlush true if that table's log is hlfush by oter means
398    * only.
399    */
400   public void setDeferredLogFlush(final boolean isDeferredLogFlush) {
401     setValue(DEFERRED_LOG_FLUSH_KEY, isDeferredLogFlush? TRUE: FALSE);
402   }
403 
404   /** @return name of table */
405   public byte [] getName() {
406     return name;
407   }
408 
409   /** @return name of table */
410   public String getNameAsString() {
411     return this.nameAsString;
412   }
413 
414   /** @return max hregion size for table */
415   public long getMaxFileSize() {
416     byte [] value = getValue(MAX_FILESIZE_KEY);
417     if (value != null)
418       return Long.valueOf(Bytes.toString(value)).longValue();
419     return HConstants.DEFAULT_MAX_FILE_SIZE;
420   }
421 
422   /** @param name name of table */
423   public void setName(byte[] name) {
424     this.name = name;
425   }
426 
427   /**
428    * @param maxFileSize The maximum file size that a store file can grow to
429    * before a split is triggered.
430    */
431   public void setMaxFileSize(long maxFileSize) {
432     setValue(MAX_FILESIZE_KEY, Bytes.toBytes(Long.toString(maxFileSize)));
433   }
434 
435   /**
436    * @return memory cache flush size for each hregion
437    */
438   public long getMemStoreFlushSize() {
439     byte [] value = getValue(MEMSTORE_FLUSHSIZE_KEY);
440     if (value != null)
441       return Long.valueOf(Bytes.toString(value)).longValue();
442     return DEFAULT_MEMSTORE_FLUSH_SIZE;
443   }
444 
445   /**
446    * @param memstoreFlushSize memory cache flush size for each hregion
447    */
448   public void setMemStoreFlushSize(long memstoreFlushSize) {
449     setValue(MEMSTORE_FLUSHSIZE_KEY,
450       Bytes.toBytes(Long.toString(memstoreFlushSize)));
451   }
452 
453   /**
454    * Adds a column family.
455    * @param family HColumnDescriptor of familyto add.
456    */
457   public void addFamily(final HColumnDescriptor family) {
458     if (family.getName() == null || family.getName().length <= 0) {
459       throw new NullPointerException("Family name cannot be null or empty");
460     }
461     this.families.put(family.getName(), family);
462   }
463 
464   /**
465    * Checks to see if this table contains the given column family
466    * @param c Family name or column name.
467    * @return true if the table contains the specified family name
468    */
469   public boolean hasFamily(final byte [] c) {
470     return families.containsKey(c);
471   }
472 
473   /**
474    * @return Name of this table and then a map of all of the column family
475    * descriptors.
476    * @see #getNameAsString()
477    */
478   @Override
479   public String toString() {
480     StringBuilder s = new StringBuilder();
481     s.append('{');
482     s.append(HConstants.NAME);
483     s.append(" => '");
484     s.append(Bytes.toString(name));
485     s.append("'");
486     for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e:
487         values.entrySet()) {
488       String key = Bytes.toString(e.getKey().get());
489       String value = Bytes.toString(e.getValue().get());
490       if (key == null) {
491         continue;
492       }
493       String upperCase = key.toUpperCase();
494       if (upperCase.equals(IS_ROOT) || upperCase.equals(IS_META)) {
495         // Skip. Don't bother printing out read-only values if false.
496         if (value.toLowerCase().equals(Boolean.FALSE.toString())) {
497           continue;
498         }
499       }
500       s.append(", ");
501       s.append(Bytes.toString(e.getKey().get()));
502       s.append(" => '");
503       s.append(Bytes.toString(e.getValue().get()));
504       s.append("'");
505     }
506     s.append(", ");
507     s.append(FAMILIES);
508     s.append(" => ");
509     s.append(families.values());
510     s.append('}');
511     return s.toString();
512   }
513 
514   /**
515    * @see java.lang.Object#equals(java.lang.Object)
516    */
517   @Override
518   public boolean equals(Object obj) {
519     if (this == obj) {
520       return true;
521     }
522     if (obj == null) {
523       return false;
524     }
525     if (!(obj instanceof HTableDescriptor)) {
526       return false;
527     }
528     return compareTo((HTableDescriptor)obj) == 0;
529   }
530 
531   /**
532    * @see java.lang.Object#hashCode()
533    */
534   @Override
535   public int hashCode() {
536     int result = Bytes.hashCode(this.name);
537     result ^= Byte.valueOf(TABLE_DESCRIPTOR_VERSION).hashCode();
538     if (this.families != null && this.families.size() > 0) {
539       for (HColumnDescriptor e: this.families.values()) {
540         result ^= e.hashCode();
541       }
542     }
543     result ^= values.hashCode();
544     return result;
545   }
546 
547   // Writable
548 
549   public void readFields(DataInput in) throws IOException {
550     int version = in.readInt();
551     if (version < 3)
552       throw new IOException("versions < 3 are not supported (and never existed!?)");
553     // version 3+
554     name = Bytes.readByteArray(in);
555     nameAsString = Bytes.toString(this.name);
556     setRootRegion(in.readBoolean());
557     setMetaRegion(in.readBoolean());
558     values.clear();
559     int numVals = in.readInt();
560     for (int i = 0; i < numVals; i++) {
561       ImmutableBytesWritable key = new ImmutableBytesWritable();
562       ImmutableBytesWritable value = new ImmutableBytesWritable();
563       key.readFields(in);
564       value.readFields(in);
565       values.put(key, value);
566     }
567     families.clear();
568     int numFamilies = in.readInt();
569     for (int i = 0; i < numFamilies; i++) {
570       HColumnDescriptor c = new HColumnDescriptor();
571       c.readFields(in);
572       families.put(c.getName(), c);
573     }
574     if (version < 4) {
575       return;
576     }
577   }
578 
579   public void write(DataOutput out) throws IOException {
580 	out.writeInt(TABLE_DESCRIPTOR_VERSION);
581     Bytes.writeByteArray(out, name);
582     out.writeBoolean(isRootRegion());
583     out.writeBoolean(isMetaRegion());
584     out.writeInt(values.size());
585     for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e:
586         values.entrySet()) {
587       e.getKey().write(out);
588       e.getValue().write(out);
589     }
590     out.writeInt(families.size());
591     for(Iterator<HColumnDescriptor> it = families.values().iterator();
592         it.hasNext(); ) {
593       HColumnDescriptor family = it.next();
594       family.write(out);
595     }
596   }
597 
598   // Comparable
599 
600   public int compareTo(final HTableDescriptor other) {
601     int result = Bytes.compareTo(this.name, other.name);
602     if (result == 0) {
603       result = families.size() - other.families.size();
604     }
605     if (result == 0 && families.size() != other.families.size()) {
606       result = Integer.valueOf(families.size()).compareTo(
607           Integer.valueOf(other.families.size()));
608     }
609     if (result == 0) {
610       for (Iterator<HColumnDescriptor> it = families.values().iterator(),
611           it2 = other.families.values().iterator(); it.hasNext(); ) {
612         result = it.next().compareTo(it2.next());
613         if (result != 0) {
614           break;
615         }
616       }
617     }
618     if (result == 0) {
619       // punt on comparison for ordering, just calculate difference
620       result = this.values.hashCode() - other.values.hashCode();
621       if (result < 0)
622         result = -1;
623       else if (result > 0)
624         result = 1;
625     }
626     return result;
627   }
628 
629   /**
630    * @return Immutable sorted map of families.
631    */
632   public Collection<HColumnDescriptor> getFamilies() {
633     return Collections.unmodifiableCollection(this.families.values());
634   }
635 
636   /**
637    * @return Immutable sorted set of the keys of the families.
638    */
639   public Set<byte[]> getFamiliesKeys() {
640     return Collections.unmodifiableSet(this.families.keySet());
641   }
642 
643   public HColumnDescriptor[] getColumnFamilies() {
644     return getFamilies().toArray(new HColumnDescriptor[0]);
645   }
646 
647   /**
648    * @param column
649    * @return Column descriptor for the passed family name or the family on
650    * passed in column.
651    */
652   public HColumnDescriptor getFamily(final byte [] column) {
653     return this.families.get(column);
654   }
655 
656   /**
657    * @param column
658    * @return Column descriptor for the passed family name or the family on
659    * passed in column.
660    */
661   public HColumnDescriptor removeFamily(final byte [] column) {
662     return this.families.remove(column);
663   }
664 
665   /**
666    * @param rootdir qualified path of HBase root directory
667    * @param tableName name of table
668    * @return path for table
669    */
670   public static Path getTableDir(Path rootdir, final byte [] tableName) {
671     return new Path(rootdir, Bytes.toString(tableName));
672   }
673 
674   /** Table descriptor for <core>-ROOT-</code> catalog table */
675   public static final HTableDescriptor ROOT_TABLEDESC = new HTableDescriptor(
676       HConstants.ROOT_TABLE_NAME,
677       new HColumnDescriptor[] { new HColumnDescriptor(HConstants.CATALOG_FAMILY,
678           10,  // Ten is arbitrary number.  Keep versions to help debuggging.
679           Compression.Algorithm.NONE.getName(), true, true, 8 * 1024,
680           HConstants.FOREVER, StoreFile.BloomType.NONE.toString(),  
681           HConstants.REPLICATION_SCOPE_LOCAL) });
682 
683   /** Table descriptor for <code>.META.</code> catalog table */
684   public static final HTableDescriptor META_TABLEDESC = new HTableDescriptor(
685       HConstants.META_TABLE_NAME, new HColumnDescriptor[] {
686           new HColumnDescriptor(HConstants.CATALOG_FAMILY,
687             10, // Ten is arbitrary number.  Keep versions to help debuggging.
688             Compression.Algorithm.NONE.getName(), true, true, 8 * 1024,
689             HConstants.FOREVER, StoreFile.BloomType.NONE.toString(),
690             HConstants.REPLICATION_SCOPE_LOCAL)});
691 }