    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one
    3    * or more contributor license agreements.  See the NOTICE file
    4    * distributed with this work for additional information
    5    * regarding copyright ownership.  The ASF licenses this file
    6    * to you under the Apache License, Version 2.0 (the
    7    * "License"); you may not use this file except in compliance
    8    * with the License.  You may obtain a copy of the License at
    9    *
    10   *   http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing,
    13   * software distributed under the License is distributed on an
    14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   * KIND, either express or implied.  See the License for the
    16   * specific language governing permissions and limitations
    17   * under the License.
    18   */
    19  package org.apache.felix.bundleplugin;
    20
    21  import java.io.IOException;
    22  import java.io.OutputStream;
    23  import java.util.Arrays;
    24  import java.util.HashSet;
    25  import java.util.Map;
    26  import java.util.Set;
    27  import java.util.TreeMap;
    28  import java.util.TreeSet;
    29  import java.util.jar.Attributes;
    30  import java.util.jar.Manifest;
    31
    32  import org.apache.felix.utils.manifest.Parser;
    33  import org.osgi.framework.Constants;
    34
    35  public class ManifestWriter {
    36
    37      /**
    38       * Unfortunately we have to write our own manifest :-( because of a stupid
    39       * bug in the manifest code. It tries to handle UTF-8 but the way it does it
    40       * it makes the bytes platform dependent. So the following code outputs the
    41       * manifest. A Manifest consists of
    42       *
    43       * <pre>
    44       *   'Manifest-Version: 1.0\r\n'
    45       *   main-attributes *
    46       *   \r\n
    47       *   name-section
    48       *
    49       *   main-attributes ::= attributes
    50       *   attributes      ::= key ': ' value '\r\n'
    51       *   name-section    ::= 'Name: ' name '\r\n' attributes
    52       * </pre>
    53       *
    54       * Lines in the manifest should not exceed 72 bytes (! this is where the
    55       * manifest screwed up as well when 16 bit unicodes were used).
    56       * <p>
    57       * As a bonus, we can now sort the manifest!
    58       */
    59      static byte[]	CONTINUE	= new byte[] {
    60              '\r', '\n', ' '
    61      };
    62
    63      static Set<String> NICE_HEADERS = new HashSet<String>(
    64              Arrays.asList(
    65                      Constants.IMPORT_PACKAGE,
    66                      Constants.DYNAMICIMPORT_PACKAGE,
    67                      Constants.IMPORT_SERVICE,
    68                      Constants.REQUIRE_CAPABILITY,
    69                      Constants.EXPORT_PACKAGE,
    70                      Constants.EXPORT_SERVICE,
    71                      Constants.PROVIDE_CAPABILITY
    72              )
    73      );
    74
    75      /**
    76       * Main function to output a manifest properly in UTF-8.
    77       *
    78       * @param manifest
    79       *            The manifest to output
    80       * @param out
    81       *            The output stream
    82       * @throws IOException
    83       *             when something fails
    84       */
    85      public static void outputManifest(Manifest manifest, OutputStream out, boolean nice) throws IOException {
    86          writeEntry(out, "Manifest-Version", "1.0", nice);
    87          attributes(manifest.getMainAttributes(), out, nice);
    88
    89          TreeSet<String> keys = new TreeSet<String>();
    90          for (Object o : manifest.getEntries().keySet())
    91              keys.add(o.toString());
    92
    93          for (String key : keys) {
    94              write(out, 0, "\r\n");
    95              writeEntry(out, "Name", key, nice);
    96              attributes(manifest.getAttributes(key), out, nice);
    97          }
    98          out.flush();
    99      }
    100
    101     /**
    102      * Write out an entry, handling proper unicode and line length constraints
    103      */
    104     private static void writeEntry(OutputStream out, String name, String value, boolean nice) throws IOException {
    105         if (nice && NICE_HEADERS.contains(name)) {
    106             int n = write(out, 0, name + ": ");
    107             String[] parts = Parser.parseDelimitedString(value, ",");
    108             if (parts.length > 1) {
    109                 write(out, 0, "\r\n ");
    110                 n = 1;
    111             }
    112             for (int i = 0; i < parts.length; i++) {
    113                 if (i < parts.length - 1) {
    114                     write(out, n, parts[i] + ",");
    115                     write(out, 0, "\r\n ");
    116                 } else {
    117                     write(out, n, parts[i]);
    118                     write(out, 0, "\r\n");
    119                 }
    120                 n = 1;
    121             }
    122         } else {
    123             int n = write(out, 0, name + ": ");
    124             write(out, n, value);
    125             write(out, 0, "\r\n");
    126         }
    127     }
    128
    129     /**
    130      * Convert a string to bytes with UTF8 and then output in max 72 bytes
    131      *
    132      * @param out
    133      *            the output string
    134      * @param i
    135      *            the current width
    136      * @param s
    137      *            the string to output
    138      * @return the new width
    139      * @throws IOException
    140      *             when something fails
    141      */
    142     private static int write(OutputStream out, int i, String s) throws IOException {
    143         byte[] bytes = s.getBytes("UTF8");
    144         return write(out, i, bytes);
    145     }
    146
    147     /**
    148      * Write the bytes but ensure that the line length does not exceed 72
    149      * characters. If it is more than 70 characters, we just put a cr/lf +
    150      * space.
    151      *
    152      * @param out
    153      *            The output stream
    154      * @param width
    155      *            The nr of characters output in a line before this method
    156      *            started
    157      * @param bytes
    158      *            the bytes to output
    159      * @return the nr of characters in the last line
    160      * @throws IOException
    161      *             if something fails
    162      */
    163     private static int write(OutputStream out, int width, byte[] bytes) throws IOException {
    164         int w = width;
    165         for (int i = 0; i < bytes.length; i++) {
    166             if (w >= 72) { // we need to add the \n\r!
    167                 out.write(CONTINUE);
    168                 w = 1;
    169             }
    170             out.write(bytes[i]);
    171             w++;
    172         }
    173         return w;
    174     }
    175
    176     /**
    177      * Output an Attributes map. We will sort this map before outputing.
    178      *
    179      * @param value
    180      *            the attrbutes
    181      * @param out
    182      *            the output stream
    183      * @throws IOException
    184      *             when something fails
    185      */
    186     private static void attributes(Attributes value, OutputStream out, boolean nice) throws IOException {
    187         TreeMap<String,String> map = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
    188         for (Map.Entry<Object,Object> entry : value.entrySet()) {
    189             map.put(entry.getKey().toString(), entry.getValue().toString());
    190         }
    191
    192         map.remove("Manifest-Version"); // get rid of
    193         // manifest
    194         // version
    195         for (Map.Entry<String,String> entry : map.entrySet()) {
    196             writeEntry(out, entry.getKey(), entry.getValue(), nice);
    197         }
    198     }
    199
    200     private static Manifest clean(Manifest org) {
    201
    202         Manifest result = new Manifest();
    203         for (Map.Entry< ? , ? > entry : org.getMainAttributes().entrySet()) {
    204             String nice = clean((String) entry.getValue());
    205             result.getMainAttributes().put(entry.getKey(), nice);
    206         }
    207         for (String name : org.getEntries().keySet()) {
    208             Attributes attrs = result.getAttributes(name);
    209             if (attrs == null) {
    210                 attrs = new Attributes();
    211                 result.getEntries().put(name, attrs);
    212             }
    213
    214             for (Map.Entry< ? , ? > entry : org.getAttributes(name).entrySet()) {
    215                 String nice = clean((String) entry.getValue());
    216                 attrs.put(entry.getKey(), nice);
    217             }
    218         }
    219         return result;
    220     }
    221
    222     private static String clean(String s) {
    223         StringBuilder sb = new StringBuilder(s);
    224         boolean changed = false;
    225         boolean replacedPrev = false;
    226         for ( int i=0; i<sb.length(); i++) {
    227             char c = s.charAt(i);
    228             switch(c) {
    229             case 0:
    230             case '\n':
    231             case '\r':
    232                 changed = true;
    233                 if ( !replacedPrev ) {
    234                     sb.replace(i, i+1, " ");
    235                     replacedPrev= true;
    236                 } else
    237                     sb.delete(i, i+1);
    238                 break;
    239             default:
    240                 replacedPrev = false;
    241                 break;
    242             }
    243         }
    244         if ( changed)
    245             return sb.toString();
    246         else
    247             return s;
    248     }
    249
    250 }