Home | History | Annotate | Download | only in jar
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  * Copyright (c) 1997, 2006, Oracle and/or its affiliates. All rights reserved.
      4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      5  *
      6  * This code is free software; you can redistribute it and/or modify it
      7  * under the terms of the GNU General Public License version 2 only, as
      8  * published by the Free Software Foundation.  Oracle designates this
      9  * particular file as subject to the "Classpath" exception as provided
     10  * by Oracle in the LICENSE file that accompanied this code.
     11  *
     12  * This code is distributed in the hope that it will be useful, but WITHOUT
     13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     15  * version 2 for more details (a copy is included in the LICENSE file that
     16  * accompanied this code).
     17  *
     18  * You should have received a copy of the GNU General Public License version
     19  * 2 along with this work; if not, write to the Free Software Foundation,
     20  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
     21  *
     22  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
     23  * or visit www.oracle.com if you need additional information or have any
     24  * questions.
     25  */
     26 
     27 package java.util.jar;
     28 
     29 import java.io.DataInputStream;
     30 import java.io.DataOutputStream;
     31 import java.io.IOException;
     32 import java.util.HashMap;
     33 import java.util.Map;
     34 import java.util.Set;
     35 import java.util.Collection;
     36 import java.util.AbstractSet;
     37 import java.util.Iterator;
     38 import sun.util.logging.PlatformLogger;
     39 import java.util.Comparator;
     40 import sun.misc.ASCIICaseInsensitiveComparator;
     41 
     42 /**
     43  * The Attributes class maps Manifest attribute names to associated string
     44  * values. Valid attribute names are case-insensitive, are restricted to
     45  * the ASCII characters in the set [0-9a-zA-Z_-], and cannot exceed 70
     46  * characters in length. Attribute values can contain any characters and
     47  * will be UTF8-encoded when written to the output stream.  See the
     48  * <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/jar/jar.html">JAR File Specification</a>
     49  * for more information about valid attribute names and values.
     50  *
     51  * @author  David Connelly
     52  * @see     Manifest
     53  * @since   1.2
     54  */
     55 public class Attributes implements Map<Object,Object>, Cloneable {
     56     /**
     57      * The attribute name-value mappings.
     58      */
     59     protected Map<Object,Object> map;
     60 
     61     /**
     62      * Constructs a new, empty Attributes object with default size.
     63      */
     64     public Attributes() {
     65         this(11);
     66     }
     67 
     68     /**
     69      * Constructs a new, empty Attributes object with the specified
     70      * initial size.
     71      *
     72      * @param size the initial number of attributes
     73      */
     74     public Attributes(int size) {
     75         map = new HashMap(size);
     76     }
     77 
     78     /**
     79      * Constructs a new Attributes object with the same attribute name-value
     80      * mappings as in the specified Attributes.
     81      *
     82      * @param attr the specified Attributes
     83      */
     84     public Attributes(Attributes attr) {
     85         map = new HashMap(attr);
     86     }
     87 
     88 
     89     /**
     90      * Returns the value of the specified attribute name, or null if the
     91      * attribute name was not found.
     92      *
     93      * @param name the attribute name
     94      * @return the value of the specified attribute name, or null if
     95      *         not found.
     96      */
     97     public Object get(Object name) {
     98         return map.get(name);
     99     }
    100 
    101     /**
    102      * Returns the value of the specified attribute name, specified as
    103      * a string, or null if the attribute was not found. The attribute
    104      * name is case-insensitive.
    105      * <p>
    106      * This method is defined as:
    107      * <pre>
    108      *      return (String)get(new Attributes.Name((String)name));
    109      * </pre>
    110      *
    111      * @param name the attribute name as a string
    112      * @return the String value of the specified attribute name, or null if
    113      *         not found.
    114      * @throws IllegalArgumentException if the attribute name is invalid
    115      */
    116     public String getValue(String name) {
    117         return (String)get(new Attributes.Name(name));
    118     }
    119 
    120     /**
    121      * Returns the value of the specified Attributes.Name, or null if the
    122      * attribute was not found.
    123      * <p>
    124      * This method is defined as:
    125      * <pre>
    126      *     return (String)get(name);
    127      * </pre>
    128      *
    129      * @param name the Attributes.Name object
    130      * @return the String value of the specified Attribute.Name, or null if
    131      *         not found.
    132      */
    133     public String getValue(Name name) {
    134         return (String)get(name);
    135     }
    136 
    137     /**
    138      * Associates the specified value with the specified attribute name
    139      * (key) in this Map. If the Map previously contained a mapping for
    140      * the attribute name, the old value is replaced.
    141      *
    142      * @param name the attribute name
    143      * @param value the attribute value
    144      * @return the previous value of the attribute, or null if none
    145      * @exception ClassCastException if the name is not a Attributes.Name
    146      *            or the value is not a String
    147      */
    148     public Object put(Object name, Object value) {
    149         return map.put((Attributes.Name)name, (String)value);
    150     }
    151 
    152     /**
    153      * Associates the specified value with the specified attribute name,
    154      * specified as a String. The attributes name is case-insensitive.
    155      * If the Map previously contained a mapping for the attribute name,
    156      * the old value is replaced.
    157      * <p>
    158      * This method is defined as:
    159      * <pre>
    160      *      return (String)put(new Attributes.Name(name), value);
    161      * </pre>
    162      *
    163      * @param name the attribute name as a string
    164      * @param value the attribute value
    165      * @return the previous value of the attribute, or null if none
    166      * @exception IllegalArgumentException if the attribute name is invalid
    167      */
    168     public String putValue(String name, String value) {
    169         return (String)put(new Name(name), value);
    170     }
    171 
    172     /**
    173      * Removes the attribute with the specified name (key) from this Map.
    174      * Returns the previous attribute value, or null if none.
    175      *
    176      * @param name attribute name
    177      * @return the previous value of the attribute, or null if none
    178      */
    179     public Object remove(Object name) {
    180         return map.remove(name);
    181     }
    182 
    183     /**
    184      * Returns true if this Map maps one or more attribute names (keys)
    185      * to the specified value.
    186      *
    187      * @param value the attribute value
    188      * @return true if this Map maps one or more attribute names to
    189      *         the specified value
    190      */
    191     public boolean containsValue(Object value) {
    192         return map.containsValue(value);
    193     }
    194 
    195     /**
    196      * Returns true if this Map contains the specified attribute name (key).
    197      *
    198      * @param name the attribute name
    199      * @return true if this Map contains the specified attribute name
    200      */
    201     public boolean containsKey(Object name) {
    202         return map.containsKey(name);
    203     }
    204 
    205     /**
    206      * Copies all of the attribute name-value mappings from the specified
    207      * Attributes to this Map. Duplicate mappings will be replaced.
    208      *
    209      * @param attr the Attributes to be stored in this map
    210      * @exception ClassCastException if attr is not an Attributes
    211      */
    212     public void putAll(Map<?,?> attr) {
    213         // ## javac bug?
    214         if (!Attributes.class.isInstance(attr))
    215             throw new ClassCastException();
    216         for (Map.Entry<?,?> me : (attr).entrySet())
    217             put(me.getKey(), me.getValue());
    218     }
    219 
    220     /**
    221      * Removes all attributes from this Map.
    222      */
    223     public void clear() {
    224         map.clear();
    225     }
    226 
    227     /**
    228      * Returns the number of attributes in this Map.
    229      */
    230     public int size() {
    231         return map.size();
    232     }
    233 
    234     /**
    235      * Returns true if this Map contains no attributes.
    236      */
    237     public boolean isEmpty() {
    238         return map.isEmpty();
    239     }
    240 
    241     /**
    242      * Returns a Set view of the attribute names (keys) contained in this Map.
    243      */
    244     public Set<Object> keySet() {
    245         return map.keySet();
    246     }
    247 
    248     /**
    249      * Returns a Collection view of the attribute values contained in this Map.
    250      */
    251     public Collection<Object> values() {
    252         return map.values();
    253     }
    254 
    255     /**
    256      * Returns a Collection view of the attribute name-value mappings
    257      * contained in this Map.
    258      */
    259     public Set<Map.Entry<Object,Object>> entrySet() {
    260         return map.entrySet();
    261     }
    262 
    263     /**
    264      * Compares the specified Attributes object with this Map for equality.
    265      * Returns true if the given object is also an instance of Attributes
    266      * and the two Attributes objects represent the same mappings.
    267      *
    268      * @param o the Object to be compared
    269      * @return true if the specified Object is equal to this Map
    270      */
    271     public boolean equals(Object o) {
    272         return map.equals(o);
    273     }
    274 
    275     /**
    276      * Returns the hash code value for this Map.
    277      */
    278     public int hashCode() {
    279         return map.hashCode();
    280     }
    281 
    282     /**
    283      * Returns a copy of the Attributes, implemented as follows:
    284      * <pre>
    285      *     public Object clone() { return new Attributes(this); }
    286      * </pre>
    287      * Since the attribute names and values are themselves immutable,
    288      * the Attributes returned can be safely modified without affecting
    289      * the original.
    290      */
    291     public Object clone() {
    292         return new Attributes(this);
    293     }
    294 
    295     /*
    296      * Writes the current attributes to the specified data output stream.
    297      * XXX Need to handle UTF8 values and break up lines longer than 72 bytes
    298      */
    299      void write(DataOutputStream os) throws IOException {
    300         Iterator it = entrySet().iterator();
    301         while (it.hasNext()) {
    302             Map.Entry e = (Map.Entry)it.next();
    303             StringBuffer buffer = new StringBuffer(
    304                                         ((Name)e.getKey()).toString());
    305             buffer.append(": ");
    306 
    307             String value = (String)e.getValue();
    308             if (value != null) {
    309                 byte[] vb = value.getBytes("UTF8");
    310                 value = new String(vb, 0, 0, vb.length);
    311             }
    312             buffer.append(value);
    313 
    314             buffer.append("\r\n");
    315             Manifest.make72Safe(buffer);
    316             os.writeBytes(buffer.toString());
    317         }
    318         os.writeBytes("\r\n");
    319     }
    320 
    321     /*
    322      * Writes the current attributes to the specified data output stream,
    323      * make sure to write out the MANIFEST_VERSION or SIGNATURE_VERSION
    324      * attributes first.
    325      *
    326      * XXX Need to handle UTF8 values and break up lines longer than 72 bytes
    327      */
    328     void writeMain(DataOutputStream out) throws IOException
    329     {
    330         // write out the *-Version header first, if it exists
    331         String vername = Name.MANIFEST_VERSION.toString();
    332         String version = getValue(vername);
    333         if (version == null) {
    334             vername = Name.SIGNATURE_VERSION.toString();
    335             version = getValue(vername);
    336         }
    337 
    338         if (version != null) {
    339             out.writeBytes(vername+": "+version+"\r\n");
    340         }
    341 
    342         // write out all attributes except for the version
    343         // we wrote out earlier
    344         Iterator it = entrySet().iterator();
    345         while (it.hasNext()) {
    346             Map.Entry e = (Map.Entry)it.next();
    347             String name = ((Name)e.getKey()).toString();
    348             if ((version != null) && ! (name.equalsIgnoreCase(vername))) {
    349 
    350                 StringBuffer buffer = new StringBuffer(name);
    351                 buffer.append(": ");
    352 
    353                 String value = (String)e.getValue();
    354                 if (value != null) {
    355                     byte[] vb = value.getBytes("UTF8");
    356                     value = new String(vb, 0, 0, vb.length);
    357                 }
    358                 buffer.append(value);
    359 
    360                 buffer.append("\r\n");
    361                 Manifest.make72Safe(buffer);
    362                 out.writeBytes(buffer.toString());
    363             }
    364         }
    365         out.writeBytes("\r\n");
    366     }
    367 
    368     /*
    369      * Reads attributes from the specified input stream.
    370      * XXX Need to handle UTF8 values.
    371      */
    372     void read(Manifest.FastInputStream is, byte[] lbuf) throws IOException {
    373         String name = null, value = null;
    374         byte[] lastline = null;
    375 
    376         int len;
    377         while ((len = is.readLine(lbuf)) != -1) {
    378             boolean lineContinued = false;
    379             if (lbuf[--len] != '\n') {
    380                 throw new IOException("line too long");
    381             }
    382             if (len > 0 && lbuf[len-1] == '\r') {
    383                 --len;
    384             }
    385             if (len == 0) {
    386                 break;
    387             }
    388             int i = 0;
    389             if (lbuf[0] == ' ') {
    390                 // continuation of previous line
    391                 if (name == null) {
    392                     throw new IOException("misplaced continuation line");
    393                 }
    394                 lineContinued = true;
    395                 byte[] buf = new byte[lastline.length + len - 1];
    396                 System.arraycopy(lastline, 0, buf, 0, lastline.length);
    397                 System.arraycopy(lbuf, 1, buf, lastline.length, len - 1);
    398                 if (is.peek() == ' ') {
    399                     lastline = buf;
    400                     continue;
    401                 }
    402                 value = new String(buf, 0, buf.length, "UTF8");
    403                 lastline = null;
    404             } else {
    405                 while (lbuf[i++] != ':') {
    406                     if (i >= len) {
    407                         throw new IOException("invalid header field");
    408                     }
    409                 }
    410                 if (lbuf[i++] != ' ') {
    411                     throw new IOException("invalid header field");
    412                 }
    413                 name = new String(lbuf, 0, 0, i - 2);
    414                 if (is.peek() == ' ') {
    415                     lastline = new byte[len - i];
    416                     System.arraycopy(lbuf, i, lastline, 0, len - i);
    417                     continue;
    418                 }
    419                 value = new String(lbuf, i, len - i, "UTF8");
    420             }
    421             try {
    422                 if ((putValue(name, value) != null) && (!lineContinued)) {
    423                     PlatformLogger.getLogger("java.util.jar").warning(
    424                                      "Duplicate name in Manifest: " + name
    425                                      + ".\n"
    426                                      + "Ensure that the manifest does not "
    427                                      + "have duplicate entries, and\n"
    428                                      + "that blank lines separate "
    429                                      + "individual sections in both your\n"
    430                                      + "manifest and in the META-INF/MANIFEST.MF "
    431                                      + "entry in the jar file.");
    432                 }
    433             } catch (IllegalArgumentException e) {
    434                 throw new IOException("invalid header field name: " + name);
    435             }
    436         }
    437     }
    438 
    439     /**
    440      * The Attributes.Name class represents an attribute name stored in
    441      * this Map. Valid attribute names are case-insensitive, are restricted
    442      * to the ASCII characters in the set [0-9a-zA-Z_-], and cannot exceed
    443      * 70 characters in length. Attribute values can contain any characters
    444      * and will be UTF8-encoded when written to the output stream.  See the
    445      * <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/jar/jar.html">JAR File Specification</a>
    446      * for more information about valid attribute names and values.
    447      */
    448     public static class Name {
    449         private String name;
    450         private int hashCode = -1;
    451 
    452         /**
    453          * Constructs a new attribute name using the given string name.
    454          *
    455          * @param name the attribute string name
    456          * @exception IllegalArgumentException if the attribute name was
    457          *            invalid
    458          * @exception NullPointerException if the attribute name was null
    459          */
    460         public Name(String name) {
    461             if (name == null) {
    462                 throw new NullPointerException("name");
    463             }
    464             if (!isValid(name)) {
    465                 throw new IllegalArgumentException(name);
    466             }
    467             this.name = name.intern();
    468         }
    469 
    470         private static boolean isValid(String name) {
    471             int len = name.length();
    472             if (len > 70 || len == 0) {
    473                 return false;
    474             }
    475             for (int i = 0; i < len; i++) {
    476                 if (!isValid(name.charAt(i))) {
    477                     return false;
    478                 }
    479             }
    480             return true;
    481         }
    482 
    483         private static boolean isValid(char c) {
    484             return isAlpha(c) || isDigit(c) || c == '_' || c == '-';
    485         }
    486 
    487         private static boolean isAlpha(char c) {
    488             return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
    489         }
    490 
    491         private static boolean isDigit(char c) {
    492             return c >= '0' && c <= '9';
    493         }
    494 
    495         /**
    496          * Compares this attribute name to another for equality.
    497          * @param o the object to compare
    498          * @return true if this attribute name is equal to the
    499          *         specified attribute object
    500          */
    501         public boolean equals(Object o) {
    502             if (o instanceof Name) {
    503                 Comparator c = ASCIICaseInsensitiveComparator.CASE_INSENSITIVE_ORDER;
    504                 return c.compare(name, ((Name)o).name) == 0;
    505             } else {
    506                 return false;
    507             }
    508         }
    509 
    510         /**
    511          * Computes the hash value for this attribute name.
    512          */
    513         public int hashCode() {
    514             if (hashCode == -1) {
    515                 hashCode = ASCIICaseInsensitiveComparator.lowerCaseHashCode(name);
    516             }
    517             return hashCode;
    518         }
    519 
    520         /**
    521          * Returns the attribute name as a String.
    522          */
    523         public String toString() {
    524             return name;
    525         }
    526 
    527         /**
    528          * <code>Name</code> object for <code>Manifest-Version</code>
    529          * manifest attribute. This attribute indicates the version number
    530          * of the manifest standard to which a JAR file's manifest conforms.
    531          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/jar/jar.html#JAR Manifest">
    532          *      Manifest and Signature Specification</a>
    533          */
    534         public static final Name MANIFEST_VERSION = new Name("Manifest-Version");
    535 
    536         /**
    537          * <code>Name</code> object for <code>Signature-Version</code>
    538          * manifest attribute used when signing JAR files.
    539          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/jar/jar.html#JAR Manifest">
    540          *      Manifest and Signature Specification</a>
    541          */
    542         public static final Name SIGNATURE_VERSION = new Name("Signature-Version");
    543 
    544         /**
    545          * <code>Name</code> object for <code>Content-Type</code>
    546          * manifest attribute.
    547          */
    548         public static final Name CONTENT_TYPE = new Name("Content-Type");
    549 
    550         /**
    551          * <code>Name</code> object for <code>Class-Path</code>
    552          * manifest attribute. Bundled extensions can use this attribute
    553          * to find other JAR files containing needed classes.
    554          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/extensions/spec.html#bundled">
    555          *      Extensions Specification</a>
    556          */
    557         public static final Name CLASS_PATH = new Name("Class-Path");
    558 
    559         /**
    560          * <code>Name</code> object for <code>Main-Class</code> manifest
    561          * attribute used for launching applications packaged in JAR files.
    562          * The <code>Main-Class</code> attribute is used in conjunction
    563          * with the <code>-jar</code> command-line option of the
    564          * <tt>java</tt> application launcher.
    565          */
    566         public static final Name MAIN_CLASS = new Name("Main-Class");
    567 
    568         /**
    569          * <code>Name</code> object for <code>Sealed</code> manifest attribute
    570          * used for sealing.
    571          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/extensions/spec.html#sealing">
    572          *      Extension Sealing</a>
    573          */
    574         public static final Name SEALED = new Name("Sealed");
    575 
    576        /**
    577          * <code>Name</code> object for <code>Extension-List</code> manifest attribute
    578          * used for declaring dependencies on installed extensions.
    579          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/extensions/spec.html#dependency">
    580          *      Installed extension dependency</a>
    581          */
    582         public static final Name EXTENSION_LIST = new Name("Extension-List");
    583 
    584         /**
    585          * <code>Name</code> object for <code>Extension-Name</code> manifest attribute
    586          * used for declaring dependencies on installed extensions.
    587          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/extensions/spec.html#dependency">
    588          *      Installed extension dependency</a>
    589          */
    590         public static final Name EXTENSION_NAME = new Name("Extension-Name");
    591 
    592         /**
    593          * <code>Name</code> object for <code>Extension-Name</code> manifest attribute
    594          * used for declaring dependencies on installed extensions.
    595          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/extensions/spec.html#dependency">
    596          *      Installed extension dependency</a>
    597          */
    598         public static final Name EXTENSION_INSTALLATION = new Name("Extension-Installation");
    599 
    600         /**
    601          * <code>Name</code> object for <code>Implementation-Title</code>
    602          * manifest attribute used for package versioning.
    603          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    604          *      Java Product Versioning Specification</a>
    605          */
    606         public static final Name IMPLEMENTATION_TITLE = new Name("Implementation-Title");
    607 
    608         /**
    609          * <code>Name</code> object for <code>Implementation-Version</code>
    610          * manifest attribute used for package versioning.
    611          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    612          *      Java Product Versioning Specification</a>
    613          */
    614         public static final Name IMPLEMENTATION_VERSION = new Name("Implementation-Version");
    615 
    616         /**
    617          * <code>Name</code> object for <code>Implementation-Vendor</code>
    618          * manifest attribute used for package versioning.
    619          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    620          *      Java Product Versioning Specification</a>
    621          */
    622         public static final Name IMPLEMENTATION_VENDOR = new Name("Implementation-Vendor");
    623 
    624         /**
    625          * <code>Name</code> object for <code>Implementation-Vendor-Id</code>
    626          * manifest attribute used for package versioning.
    627          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    628          *      Java Product Versioning Specification</a>
    629          */
    630         public static final Name IMPLEMENTATION_VENDOR_ID = new Name("Implementation-Vendor-Id");
    631 
    632        /**
    633          * <code>Name</code> object for <code>Implementation-Vendor-URL</code>
    634          * manifest attribute used for package versioning.
    635          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    636          *      Java Product Versioning Specification</a>
    637          */
    638         public static final Name IMPLEMENTATION_URL = new Name("Implementation-URL");
    639 
    640         /**
    641          * <code>Name</code> object for <code>Specification-Title</code>
    642          * manifest attribute used for package versioning.
    643          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    644          *      Java Product Versioning Specification</a>
    645          */
    646         public static final Name SPECIFICATION_TITLE = new Name("Specification-Title");
    647 
    648         /**
    649          * <code>Name</code> object for <code>Specification-Version</code>
    650          * manifest attribute used for package versioning.
    651          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    652          *      Java Product Versioning Specification</a>
    653          */
    654         public static final Name SPECIFICATION_VERSION = new Name("Specification-Version");
    655 
    656         /**
    657          * <code>Name</code> object for <code>Specification-Vendor</code>
    658          * manifest attribute used for package versioning.
    659          * @see <a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/versioning/spec/versioning2.html#wp90779">
    660          *      Java Product Versioning Specification</a>
    661          */
    662         public static final Name SPECIFICATION_VENDOR = new Name("Specification-Vendor");
    663 
    664         /**
    665          * @hide
    666          */
    667         public static final Name NAME = new Name("Name");
    668     }
    669 }
    670