001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.apisupport.project;
043:
044: import java.io.BufferedReader;
045: import java.io.IOException;
046: import java.io.InputStream;
047: import java.io.InputStreamReader;
048: import java.io.OutputStream;
049: import java.io.OutputStreamWriter;
050: import java.io.Writer;
051: import java.util.ArrayList;
052: import java.util.Collections;
053: import java.util.HashSet;
054: import java.util.Iterator;
055: import java.util.LinkedList;
056: import java.util.List;
057: import java.util.Locale;
058: import java.util.Set;
059: import java.util.regex.Matcher;
060: import java.util.regex.Pattern;
061:
062: /**
063: * Similar to {@link java.util.jar.Manifest} but preserves all formatting when changes are made.
064: * Methods which take a section name can accept null for the main section.
065: * (This style is in contrast to that used in {@link java.util.jar.Attributes}.
066: * Does not implement esoteric aspects of the manifest spec such as signature recognition
067: * and line wrapping after 72 characters, but does produce output which can be read
068: * by {@link java.util.jar.Manifest} and Ant's <code><jar></code> task.
069: * Will not touch the formatting of any line unless you ask to change it, nor
070: * reorder lines, etc., except to correct line endings or insert a final newline.
071: * Newly added sections and attributes are inserted in alphabetical order.
072: * @author Jesse Glick
073: */
074: public final class EditableManifest {
075:
076: private static final String MANIFEST_VERSION = "Manifest-Version"; // NOI18N
077: private static final String MANIFEST_VERSION_VALUE = "1.0"; // NOI18N
078:
079: private final Section mainSection;
080: private final List<Section> sections;
081:
082: /**
083: * Creates an almost empty manifest.
084: * Contains just <code>Manifest-Version: 1.0</code>.
085: */
086: public EditableManifest() {
087: try {
088: mainSection = new Section(Collections
089: .singletonList(new Line(MANIFEST_VERSION,
090: MANIFEST_VERSION_VALUE)), true, 1);
091: } catch (IOException e) {
092: throw new AssertionError(e);
093: }
094: sections = new ArrayList<Section>();
095: }
096:
097: /**
098: * Creates a manifest object from an existing manifest file.
099: * @param is a stream to load content from (in UTF-8 encoding)
100: * @throws IOException if reading the stream failed, or the contents were syntactically malformed
101: */
102: public EditableManifest(InputStream is) throws IOException {
103: BufferedReader r = new BufferedReader(new InputStreamReader(is,
104: "UTF-8")); // NOI18N
105: sections = new LinkedList<Section>();
106: String text;
107: int blankLines = 0;
108: List<Line> lines = new ArrayList<Line>();
109: Section _mainSection = null;
110: while (true) {
111: text = r.readLine();
112: if (text == null || (text.length() > 0 && blankLines > 0)) {
113: Section s = new Section(lines, _mainSection == null,
114: blankLines);
115: if (_mainSection == null) {
116: _mainSection = s;
117: } else {
118: sections.add(s);
119: }
120: lines.clear();
121: blankLines = 0;
122: }
123: if (text != null) {
124: if (text.length() > 0) {
125: Line line;
126: if (text.charAt(0) == ' ') {
127: if (lines.isEmpty()) {
128: throw new IOException(
129: "Continuation lines only allowed for attributes"); // NOI18N
130: }
131: Line prev = lines.remove(lines.size() - 1);
132: line = new Line(prev.name, prev.value
133: + text.substring(1), prev.text
134: + System.getProperty("line.separator")
135: + text);
136: } else {
137: line = new Line(text);
138: }
139: lines.add(line);
140: } else {
141: blankLines++;
142: }
143: } else {
144: break;
145: }
146: }
147: mainSection = _mainSection;
148: Set<String> names = new HashSet<String>();
149: for (Section s : sections) {
150: if (!names.add(s.name)) {
151: throw new IOException("Duplicated section names: "
152: + s.name); // NOI18N
153: }
154: }
155: }
156:
157: /**
158: * Stores the manifest to a file.
159: * @param os a stream to write content to (in UTF-8 encoding, using platform default line endings)
160: * @throws IOException if writing to the stream failed
161: */
162: public void write(OutputStream os) throws IOException {
163: Writer w = new OutputStreamWriter(os, "UTF-8"); // NOI18N
164: mainSection.write(w, !sections.isEmpty());
165: Iterator<Section> it = sections.iterator();
166: while (it.hasNext()) {
167: it.next().write(w, it.hasNext());
168: }
169: w.flush();
170: }
171:
172: /**
173: * Adds a new section.
174: * It will be added in alphabetical order relative to other sections, if they
175: * are already alphabetized.
176: * @param name the new section name
177: * @throws IllegalArgumentException if a section with that name already existed
178: */
179: public void addSection(String name) throws IllegalArgumentException {
180: if (name == null) {
181: throw new IllegalArgumentException();
182: }
183: if (findSection(name) != null) {
184: throw new IllegalArgumentException(name);
185: }
186: int i;
187: for (i = 0; i < sections.size(); i++) {
188: Section s = sections.get(i);
189: if (s.name.compareTo(name) > 0) {
190: break;
191: }
192: }
193: sections.add(i, new Section(name));
194: }
195:
196: /**
197: * Removes a section.
198: * @param name the section name to delete
199: * @throws IllegalArgumentException if there was no such section
200: */
201: public void removeSection(String name)
202: throws IllegalArgumentException {
203: if (name == null) {
204: throw new IllegalArgumentException();
205: }
206: Iterator<Section> it = sections.iterator();
207: while (it.hasNext()) {
208: Section s = it.next();
209: if (s.name.equals(name)) {
210: it.remove();
211: return;
212: }
213: }
214: throw new IllegalArgumentException(name);
215: }
216:
217: /**
218: * Gets a list of all named sections (not including the main section).
219: * @return a list of section names
220: */
221: public Set<String> getSectionNames() {
222: Set<String> names = new HashSet<String>();
223: for (Section s : sections) {
224: names.add(s.name);
225: }
226: return names;
227: }
228:
229: private Section findSection(String section) {
230: if (section == null) {
231: return mainSection;
232: } else {
233: for (Section s : sections) {
234: if (s.name.equals(section)) {
235: return s;
236: }
237: }
238: return null;
239: }
240: }
241:
242: /**
243: * Find the value of an attribute.
244: * @param name the attribute name (case-insensitive)
245: * @param section the name of the section to look in, or null for the main section
246: * @return the attribute value, or null if not defined
247: * @throws IllegalArgumentException if the named section does not exist
248: */
249: public String getAttribute(String name, String section)
250: throws IllegalArgumentException {
251: Section s = findSection(section);
252: if (s == null) {
253: throw new IllegalArgumentException(section);
254: }
255: return s.getAttribute(name);
256: }
257:
258: /**
259: * Changes the value of an attribute, or adds the attribute if it does not yet exist.
260: * If it is being added, it will be added in alphabetical order relative to
261: * other attributes in the same section, if they are already alphabetized.
262: * @param name the attribute name (case-insensitive if it already exists)
263: * @param value the new attribute value
264: * @param section the name of the section to add it to, or null for the main section
265: * @throws IllegalArgumentException if the named section does not exist
266: */
267: public void setAttribute(String name, String value, String section)
268: throws IllegalArgumentException {
269: Section s = findSection(section);
270: if (s == null) {
271: throw new IllegalArgumentException(section);
272: }
273: s.setAttribute(name, value);
274: }
275:
276: /**
277: * Removes an attribute.
278: * @param name the attribute name to delete (case-insensitive)
279: * @param section the name of the section to remove it from, or null for the main section
280: * @throws IllegalArgumentException if the named section or attribute do not exist
281: */
282: public void removeAttribute(String name, String section)
283: throws IllegalArgumentException {
284: Section s = findSection(section);
285: if (s == null) {
286: throw new IllegalArgumentException(section);
287: }
288: s.removeAttribute(name);
289: }
290:
291: /**
292: * Gets a list of all attributes.
293: * @param section the name of the section to examine, or null for the main section
294: * @throws IllegalArgumentException if the named section does not exist
295: */
296: public Set<String> getAttributeNames(String section)
297: throws IllegalArgumentException {
298: Section s = findSection(section);
299: if (s == null) {
300: throw new IllegalArgumentException(section);
301: }
302: return s.getAttributeNames();
303: }
304:
305: private static final class Line {
306:
307: private static final Pattern NAME_VALUE = Pattern
308: .compile("([^: ]+) *: *(.*)"); // NOI18N
309:
310: public final String text;
311: public final String name;
312: public final String value;
313:
314: public Line(String text) throws IOException {
315: this .text = text;
316: assert text.length() > 0;
317: Matcher m = NAME_VALUE.matcher(text);
318: if (m.matches()) {
319: name = m.group(1);
320: value = m.group(2);
321: } else {
322: throw new IOException("Malformed line: " + text); // NOI18N
323: }
324: }
325:
326: public Line(String name, String value) {
327: this (name, value, name + ": " + value); // NOI18N
328: }
329:
330: public Line(String name, String value, String text) {
331: this .name = name;
332: this .value = value;
333: this .text = text;
334: }
335:
336: public void write(Writer w) throws IOException {
337: w.write(text);
338: newline(w);
339: }
340:
341: }
342:
343: private static void newline(Writer w) throws IOException {
344: w.write(System.getProperty("line.separator")); // NOI18N
345: }
346:
347: private static final class Section {
348:
349: private static final String NAME = "Name"; // NOI18N
350:
351: public final String name;
352: private final List<Line> lines;
353: private final int blankLinesAfter;
354:
355: public Section(List<Line> lines, boolean main,
356: int blankLinesAfter) throws IOException {
357: this .lines = new ArrayList<Line>(lines);
358: this .blankLinesAfter = blankLinesAfter;
359: if (main) {
360: name = null;
361: if (!lines.isEmpty()) {
362: Line first = lines.get(0);
363: if (first.name.equalsIgnoreCase(NAME)) {
364: throw new IOException(
365: "Cannot start with a named section"); // NOI18N
366: }
367: }
368: } else {
369: assert !lines.isEmpty();
370: Line first = lines.get(0);
371: if (!first.name.equalsIgnoreCase(NAME)) {
372: throw new IOException("Section did not start with "
373: + NAME); // NOI18N
374: }
375: name = first.value;
376: if (name.length() == 0) {
377: throw new IOException(
378: "Cannot have a blank section name"); // NOI18N
379: }
380: }
381: Set<String> attrNames = new HashSet<String>();
382: Iterator<Line> it = lines.iterator();
383: if (!main) {
384: it.next();
385: }
386: while (it.hasNext()) {
387: String name = it.next().name;
388: if (name.equals(NAME)) {
389: throw new IOException(
390: "Sections not separated by blank lines"); // NOI18N
391: } else if (!attrNames.add(name.toLowerCase(Locale.US))) {
392: throw new IOException(
393: "Duplicated attributes in a section: "
394: + name); // NOI18N
395: }
396: }
397: }
398:
399: public Section(String name) {
400: this .name = name;
401: lines = new ArrayList<Line>();
402: lines.add(new Line(NAME, name)); // NOI18N
403: blankLinesAfter = 1;
404: }
405:
406: private Line findAttribute(String name) {
407: Iterator<Line> it = lines.iterator();
408: if (this .name != null) {
409: it.next();
410: }
411: while (it.hasNext()) {
412: Line line = it.next();
413: if (line.name.equalsIgnoreCase(name)) {
414: return line;
415: }
416: }
417: return null;
418: }
419:
420: private int findAttributeIndex(String name) {
421: for (int i = (this .name != null ? 1 : 0); i < lines.size(); i++) {
422: Line line = lines.get(i);
423: if (line.name.equalsIgnoreCase(name)) {
424: return i;
425: }
426: }
427: return -1;
428: }
429:
430: public String getAttribute(String name) {
431: Line line = findAttribute(name);
432: if (line != null) {
433: return line.value;
434: } else {
435: return null;
436: }
437: }
438:
439: public void setAttribute(String name, String value) {
440: for (int i = (this .name != null ? 1 : 0); i < lines.size(); i++) {
441: Line line = lines.get(i);
442: if (name.equalsIgnoreCase(line.name)) {
443: if (line.value.equals(value)) {
444: // No change, leave alone to preserve formatting.
445: return;
446: }
447: // Edit this line.
448: lines.remove(i);
449: int insertionPoint = name
450: .equalsIgnoreCase(MANIFEST_VERSION) ? 0 : i;
451: lines.add(insertionPoint, new Line(name, value));
452: return;
453: }
454: }
455: // Didn't find an existing line. Look for the right place to insert this one.
456: int insertionPoint;
457: if (name.equalsIgnoreCase(MANIFEST_VERSION)) {
458: insertionPoint = 0;
459: } else {
460: insertionPoint = lines.size();
461: for (int i = (this .name != null ? 1 : 0); i < lines
462: .size(); i++) {
463: Line line = lines.get(i);
464: int comp = line.name.compareToIgnoreCase(name);
465: assert comp != 0;
466: if (comp > 0
467: && !line.name
468: .equalsIgnoreCase(MANIFEST_VERSION)) {
469: insertionPoint = i;
470: break;
471: }
472: }
473: }
474: lines.add(insertionPoint, new Line(name, value));
475: }
476:
477: public void removeAttribute(String name)
478: throws IllegalArgumentException {
479: int i = findAttributeIndex(name);
480: if (i != -1) {
481: lines.remove(i);
482: } else {
483: throw new IllegalArgumentException(name);
484: }
485: }
486:
487: public Set<String> getAttributeNames() {
488: Set<String> attrNames = new HashSet<String>();
489: Iterator<Line> it = lines.iterator();
490: if (name != null) {
491: it.next();
492: }
493: while (it.hasNext()) {
494: attrNames.add(it.next().name);
495: }
496: return attrNames;
497: }
498:
499: public void write(Writer w, boolean forceBlankLine)
500: throws IOException {
501: Iterator it = lines.iterator();
502: while (it.hasNext()) {
503: Line line = (Line) it.next();
504: line.write(w);
505: }
506: for (int i = 0; i < blankLinesAfter; i++) {
507: newline(w);
508: }
509: if (forceBlankLine && blankLinesAfter == 0) {
510: newline(w);
511: }
512: }
513:
514: }
515:
516: }
|