/*
* Copyright (c) 2007 Borland Software Corporation
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Artem Tikhomirov (Borland) - initial API and implementation
*/
package org.eclipse.gmf.internal.common.codegen;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import org.eclipse.gmf.internal.common.Activator;
import org.eclipse.osgi.util.ManifestElement;
import org.osgi.framework.BundleException;
/**
* If both files has header, 3 possible solutions are: Preserve (if explicitly stated by user), Append (if multivalue), Overwrite (if singlevalue).
* If only old contents has a header, then Preserve.
* If header present only in new text (and not among ignored), Insert.
* @author artem
*/
public class ManifestFileMerge {
private static final String IGNORE_MERGE_HEADER = "GMF-IgnoreMerge"; //$NON-NLS-1$
private String[] myIgnoredHeaders;
private final String myLineSeparator;
public ManifestFileMerge() {
this(System.getProperties().getProperty("line.separator")); //$NON-NLS-1$
}
/**
* @param lineSeparator line delimeter to use when formatting output
*/
public ManifestFileMerge(String lineSeparator) {
assert lineSeparator != null;
myLineSeparator = lineSeparator;
}
public String process(String oldText, String newText) {
try {
cleanIgnoredHeaders();
LinkedHashMap<String, String> oldHeaders = new LinkedHashMap<String, String>();
ManifestElement.parseBundleManifest(new ByteArrayInputStream(oldText.getBytes("UTF8")), oldHeaders); //$NON-NLS-1$
LinkedHashMap<String, String> newHeaders = new LinkedHashMap<String, String>();
ManifestElement.parseBundleManifest(new ByteArrayInputStream(newText.getBytes("UTF8")), newHeaders); //$NON-NLS-1$
initializeIgnoredHeaders(oldHeaders);
for (String newHeader : newHeaders.keySet()) {
if (!isIgnoredHeader(newHeader)) {
if (oldHeaders.containsKey(newHeader)) {
String oldValue = oldHeaders.get(newHeader);
String newValue = newHeaders.get(newHeader);
if (isMultivalued(oldValue) || isMultivalued(newValue)) {
oldHeaders.put(newHeader, mergeMultivalued(newHeader, oldValue, newValue));
} else {
// just overwrite simple attributes
oldHeaders.put(newHeader, newValue);
}
} else {
oldHeaders.put(newHeader, newHeaders.get(newHeader));
}
}
}
return format(oldHeaders);
} catch (IOException ex) {
return newText;
} catch (BundleException ex) {
Activator.logError("Error merging MANIFEST.MF", ex); //$NON-NLS-1$
return newText;
}
}
protected String format(Map<String, String> oldHeaders) throws BundleException {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String,String> e : oldHeaders.entrySet()) {
sb.append(e.getKey());
sb.append(':');
sb.append(' ');
if (valueFitsSingleLine(e.getKey(), e.getValue())) {
sb.append(e.getValue());
} else {
sb.append(formatValue(e.getKey(), e.getValue()));
}
sb.append(myLineSeparator);
}
return sb.toString();
}
protected boolean valueFitsSingleLine(String headerHint, String value) {
// manifest.mf line is limited to 72 bytes. Though value is not necessarily
// fits into bytes (e.g. native symbols in UTF8 might be longer), with '70' we
// assume in most cases values are plain old ascii.
return headerHint.length() + 2 /* colon space */ + value.length() < 70;
}
protected CharSequence formatValue(String headerHint, String value) throws BundleException {
if (!isMultivalued(value)) {
return value;
}
ManifestElement[] values = ManifestElement.parseHeader(headerHint, value);
assert values.length > 0; // otherwise, it won't be multivalued
StringBuilder sb = new StringBuilder();
sb.append(formatValue(values[0]));
for (int i = 1; i < values.length; i++) {
sb.append(',');
sb.append(myLineSeparator);
sb.append(' ');
sb.append(formatValue(values[i]));
}
return sb;
}
protected CharSequence formatValue(ManifestElement element) {
StringBuilder sb = new StringBuilder(element.getValue());
// using tokens for directives and quoted strings for attributes
// seems to be PDE convention, though I didn't find exact code that does that.
// Without such a convention, it's very hard to tell whether original
// directive or attribute was quoted or not - specialized Tokenizer from
// ManifestElement rips this information out.
for (Enumeration<?> en = element.getDirectiveKeys(); en != null && en.hasMoreElements();) {
final String directiveKey = (String) en.nextElement();
for (String v : element.getDirectives(directiveKey)) {
sb.append(';');
sb.append(directiveKey);
sb.append(':');
sb.append('=');
sb.append(v);
}
}
for (Enumeration<?> en = element.getKeys(); en != null && en.hasMoreElements();) {
final String attrKey = (String) en.nextElement();
for (String v : element.getAttributes(attrKey)) {
sb.append(';');
sb.append(attrKey);
sb.append('=');
sb.append('"');
sb.append(v);
sb.append('"');
}
}
return sb;
}
private boolean isIgnoredHeader(String header) {
assert myIgnoredHeaders != null;
return Arrays.binarySearch(myIgnoredHeaders, header) >= 0;
}
private void initializeIgnoredHeaders(LinkedHashMap<String, String> oldHeaders) throws BundleException {
if (!oldHeaders.containsKey(IGNORE_MERGE_HEADER)) {
myIgnoredHeaders = new String[0];
return;
}
ManifestElement[] values = ManifestElement.parseHeader(IGNORE_MERGE_HEADER, oldHeaders.get(IGNORE_MERGE_HEADER));
if (values == null) {
// log info - header is there but can't parse
myIgnoredHeaders = new String[0];
return;
}
myIgnoredHeaders = new String[values.length];
for (int i = 0; i < values.length; i++) {
// XXX we may process directives like preserve, append or overwrite here later, if desired
myIgnoredHeaders[i] = values[i].getValue();
}
Arrays.sort(myIgnoredHeaders);
}
private void cleanIgnoredHeaders() {
myIgnoredHeaders = null;
}
private boolean isMultivalued(String value) {
// quick-and-dirty way. in rare cases may give false answer (i.e. when ;att="[1.0,2.0)"
// but it's ok
return value.indexOf(',') > 0;
}
/**
* TODO rewrite to return ManifestElements instead of serializing result to String
* which will be parsed once again at {@link #format(Map)}.
*/
private String mergeMultivalued(String header, String oldValue, String newValue) throws BundleException {
ManifestElement[] oldValues = ManifestElement.parseHeader(header, oldValue);
if (oldValues == null || oldValues.length == 0) {
return newValue;
}
String[] lookupValues = new String[oldValues.length]; // value parts of manifest entry only, no attributes or directives
for (int i = 0; i < oldValues.length; i++) {
lookupValues[i] = oldValues[i].getValue();
}
Arrays.sort(lookupValues);
LinkedList<ManifestElement> additionalElements = new LinkedList<ManifestElement>();
for (ManifestElement n : ManifestElement.parseHeader(header, newValue)) {
if (Arrays.binarySearch(lookupValues, n.getValue()) < 0) {
additionalElements.add(n);
}
}
StringBuilder sb = new StringBuilder();
// we don't care about newlines as this is intermediate result
for (ManifestElement me : oldValues) {
sb.append(formatValue(me));
sb.append(',');
}
for (ManifestElement me : additionalElements) {
sb.append(formatValue(me));
sb.append(',');
}
assert sb.charAt(sb.length() - 1) == ',';
sb.setLength(sb.length() - 1);
return sb.toString();
}
}