View Javadoc
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 }