/* * Copyright (c) 2006, 2009 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: * Alexander Fedorov (Borland) - initial API and implementation * Artem Tikhomirov (Borland) - members' visibility/access * [254532] further distinguish extensions by id, if present */ package org.eclipse.gmf.internal.common.codegen; import java.io.IOException; import java.io.StringReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import org.eclipse.gmf.internal.common.Activator; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /* * Please do not blame anyone for the implementation, it will be changed soon * FIXME rewrite with SAX/DOM and separate post-processing formatter */ public class PluginXMLTextMerger { private static final String ATTR_POINT = "point"; //$NON-NLS-1$ private static final String ELEM_EXTENSION = "extension"; //$NON-NLS-1$ private static final String ELEM_EXTENSION_START = "<" + ELEM_EXTENSION; //$NON-NLS-1$ private static final String ELEM_EXTENSION_END = "</" + ELEM_EXTENSION + ">"; //$NON-NLS-1$ //$NON-NLS-2$ private static final String ELEM_PLUGIN = "plugin"; //$NON-NLS-1$ private static final String ELEM_PLUGIN_END = "</" + ELEM_PLUGIN + ">"; //$NON-NLS-1$ //$NON-NLS-2$ private static final String COMMENT_START = "<!--"; //$NON-NLS-1$ private static final String COMMENT_END = "-->"; //$NON-NLS-1$ // private final String myPITarget; // private final String myPIAttrName; // private final String myPIAttrValue; private final String myPITag; private SAXParserFactory factory; public PluginXMLTextMerger(String piTarget, String piAttrName, String piAttrValue) { // this.myPITarget = piTarget; // this.myPIAttrName = piAttrName; // myPIAttrValue = piAttrValue; this.myPITag = MessageFormat.format("<?{0} {1}=\"{2}\"?>", piTarget, piAttrName, piAttrValue); //$NON-NLS-1$ } public boolean isRecognizedDocument(String xml) { try { final ParsedPluginXML doc = parseDocument(xml); return doc != null && doc.getExtensionsStart() > 0 & doc.getExtensionsEnd() >= doc.getExtensionsStart(); } catch (Exception ex) { return false; } } public String process(String oldXML, String newXML) { ParsedPluginXML newDoc; try { newDoc = parseDocument(newXML); } catch (Exception e) { logException("Generated plugin.xml is invalid. Existing plugin.xml will be kept", e); //$NON-NLS-1$ return oldXML; } ParsedPluginXML oldDoc; try { oldDoc = parseDocument(oldXML); } catch (Exception e) { logException("Existing plugin.xml is invalid and will be replaced with generated one", e); //$NON-NLS-1$ return newXML; } String result = mergeDocuments(oldDoc, newDoc); try { parseDocument(result); } catch (Exception e) { logException("Merged plugin.xml is invalid and will be replaced with generated one", e); //$NON-NLS-1$ return newXML; } return result; } private ParsedPluginXML parseDocument(String xml) throws SAXException, ParserConfigurationException, IOException { ParsedPluginXML pluginXML = new ParsedPluginXML(xml, myPITag); InputSource is = new InputSource(new StringReader(xml)); getParserFactory().newSAXParser().parse(is, pluginXML); return pluginXML; } private SAXParserFactory getParserFactory() { if (factory == null) { factory = SAXParserFactory.newInstance(); } return factory; } private String mergeDocuments(ParsedPluginXML oldDoc, ParsedPluginXML newDoc) { StringBuilder result = new StringBuilder(); int currentPosition = oldDoc.getExtensionsStart() - 1; final String oldXML = oldDoc.getXML(); result.append(oldXML.substring(0, currentPosition)); final int length = oldXML.length(); while (currentPosition < length) { int key = currentPosition; ExtensionDescriptor oldED = oldDoc.getExtensionByStart(key); if (oldED == null) { result.append(oldXML.charAt(currentPosition)); currentPosition++; } else { List<ExtensionDescriptor> newEDs = newDoc.getExtensionsByPoint(oldED.pointName); if (oldED.generated) { // if there's only one new descriptor, replace // if there's more, need to take extension's id into account, // but remove oldED anyway, regardless whether there was matching newED or not boolean foundMatched = false; for (ExtensionDescriptor ed : newEDs) { if (oldED.identityMatches(ed)) { result.append(ed.getText()); ed.remove(); foundMatched = true; break; } } if (!foundMatched && newEDs.size() > 0) { // copy one of new elements here, in effort to keep old order ExtensionDescriptor newED = newEDs.get(0); result.append(newED.getText()); newED.remove(); } currentPosition = oldED.endLine; oldED.remove(); } else { //keep result.append(oldED.getText()); currentPosition += oldED.getTextLength(); oldED.remove(); for (ExtensionDescriptor newED : newEDs) { if (newED.identityMatches(oldED)) { newED.remove(); } } } } if (!oldDoc.hasMoreExtensions() && newDoc.hasMoreExtensions()) { boolean sameStartEnd = oldDoc.getExtensionsStart() == oldDoc.getExtensionsEnd(); boolean afterStart = currentPosition >= oldDoc.getExtensionsStart(); if (afterStart && (sameStartEnd || currentPosition <= oldDoc.getExtensionsEnd())) { for (ExtensionDescriptor newED : newDoc.getExtensions()) { result.append(newED.getText()); newED.remove(); result.append(getPlatformNewLine()); } } } } return result.toString(); } protected void logException(String message, Exception e) { Activator.logError(message, e); } private static String getPlatformNewLine() { return System.getProperties().getProperty("line.separator"); //$NON-NLS-1$ } private static class ParsedPluginXML extends DefaultHandler { private final String myXML; private String myGeneratedToken; private int myPluginEnd; private final Map<String, List<ExtensionDescriptor>> myPoint2ExtensionsMap; private final SortedMap<Integer, ExtensionDescriptor> myStart2ExtensionMap; private Iterator<ExtensionDescriptor> myIterator; private final int myCachedExtStart; ParsedPluginXML(String xml, String generatedToken) { this.myXML = xml; this.myGeneratedToken = generatedToken; this.myPoint2ExtensionsMap = new HashMap<String, List<ExtensionDescriptor>>(); this.myStart2ExtensionMap = new TreeMap<Integer, ExtensionDescriptor>(); parse(xml); myCachedExtStart = myStart2ExtensionMap.size() > 0 ? myStart2ExtensionMap.firstKey() : myPluginEnd; } private void parse(String xml) { int currentIndex = 0; final int length = xml.length(); this.myPluginEnd = xml.lastIndexOf(ELEM_PLUGIN_END); while (currentIndex < length) { int extensionStart = getStartIndex(xml, ELEM_EXTENSION_START, currentIndex); if (extensionStart == length - 1) { break; } if (isInsideComment(xml, extensionStart)) { currentIndex = extensionStart + ELEM_EXTENSION_START.length(); continue; } if (!Character.isWhitespace(xml.charAt(extensionStart + ELEM_EXTENSION_START.length()))) { // e.g. "<extension-point" currentIndex = extensionStart + ELEM_EXTENSION_START.length(); continue; } currentIndex = processExtensonBlock(xml, extensionStart); } } private int processExtensonBlock(String xml, int fromIndex) { int extensionStart = fromIndex; int extensionEnd = getStartIndex(xml, ELEM_EXTENSION_END, fromIndex) + ELEM_EXTENSION_END.length(); while (isInsideComment(xml, extensionEnd)) { extensionEnd = getStartIndex(xml, ELEM_EXTENSION_END, extensionEnd) + ELEM_EXTENSION_END.length(); } // look ahead 2 (\n\r and \r\n) chars at most, if they are newline, include them into extension's range // this helps to keep user's formatting for (int i = 0; i < 2 && extensionEnd < xml.length(); i++) { if (xml.charAt(extensionEnd) == '\n' || xml.charAt(extensionEnd) == '\r') { // only \r\n or \n\r or single \n constitute a newline, // need to be careful not to treat double \n as a single newline if (i == 0 || xml.charAt(extensionEnd-1) != xml.charAt(extensionEnd)) { extensionEnd++; } } else { break; } } boolean isGenerated = isGenerated(xml, extensionStart, extensionEnd); ExtensionDescriptor ed = new ExtensionDescriptor(this, extensionStart, extensionEnd, isGenerated); myStart2ExtensionMap.put(ed.startLine, ed); return extensionEnd; } private boolean isGenerated(String xml, int extensionStart, int extensionEnd) { int genStart = getStartIndex(xml, myGeneratedToken, extensionStart); while (genStart < extensionEnd) { if (!isInsideComment(xml, genStart)) { return true; } genStart = getStartIndex(xml, myGeneratedToken, genStart + myGeneratedToken.length()); } return false; } private int getStartIndex(String xml, String token, int fromIndex) { int commentStart = xml.indexOf(token, fromIndex); return (commentStart < 0) ? xml.length()-1 : commentStart; } private boolean isInsideComment(String xml, int fromIndex) { int lastOpened = xml.lastIndexOf(COMMENT_START, fromIndex); if (lastOpened < 0) { return false; } int lastClosed = xml.lastIndexOf(COMMENT_END, fromIndex); if (lastClosed > lastOpened && lastClosed < fromIndex) { return false; } return true; } // modifiable copy of the list List<ExtensionDescriptor> getExtensionsByPoint(String point) { final List<ExtensionDescriptor> list = myPoint2ExtensionsMap.get(point); return list == null ? Collections.<ExtensionDescriptor>emptyList() : new ArrayList<ExtensionDescriptor>(list); } ExtensionDescriptor getExtensionByStart(int start) { return myStart2ExtensionMap.get(start); } void removeExtension(ExtensionDescriptor ed) { myStart2ExtensionMap.remove(ed.startLine); List<ExtensionDescriptor> list = myPoint2ExtensionsMap.get(ed.pointName); if (list != null) { list.remove(ed); if (list.size() == 0) { myPoint2ExtensionsMap.remove(ed.pointName); } } } boolean hasMoreExtensions() { return !myStart2ExtensionMap.isEmpty(); } List<ExtensionDescriptor> getExtensions() { return new ArrayList<ExtensionDescriptor>(myStart2ExtensionMap.values()); } int getExtensionsStart() { if (myStart2ExtensionMap.size() > 0) { return myStart2ExtensionMap.firstKey(); } return myCachedExtStart; } int getExtensionsEnd() { if (myStart2ExtensionMap.size() > 0) { ExtensionDescriptor ed = myStart2ExtensionMap.get(myStart2ExtensionMap.lastKey()); return ed.endLine; } return myPluginEnd; } String getXML() { return this.myXML; } @Override public void startDocument() throws SAXException { myIterator = myStart2ExtensionMap.values().iterator(); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (ELEM_EXTENSION.equals(qName)) { String pointName = attributes.getValue(ATTR_POINT); String identity = attributes.getValue("id"); //$NON-NLS-1$ if (pointName != null) { if (myIterator != null && myIterator.hasNext()) { ExtensionDescriptor ed = myIterator.next(); ed.pointName = pointName; ed.identity = identity; List<ExtensionDescriptor> list = myPoint2ExtensionsMap.get(ed.pointName); if (list == null) { list = new LinkedList<ExtensionDescriptor>(); myPoint2ExtensionsMap.put(ed.pointName, list); } list.add(ed); } } } } @Override public void endDocument() throws SAXException { myIterator = null; } } private static class ExtensionDescriptor { private final ParsedPluginXML parsedDoc; String pointName; String identity; final boolean generated; private final int startLine; private final int endLine; ExtensionDescriptor(ParsedPluginXML parsedPluginXml, int start, int end, boolean isGenerated) { parsedDoc = parsedPluginXml; startLine = start; endLine = end; generated = isGenerated; } String getText() { return parsedDoc.getXML().substring(startLine, endLine); } int getTextLength() { return endLine - startLine; } void remove() { parsedDoc.removeExtension(this); } boolean identityMatches(ExtensionDescriptor other) { return identity == null ? other.identity == null : identity.equals(other.identity); } } }