/******************************************************************************* * Copyright (c) 2000, 2010 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.equinox.internal.p2.updatesite; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.*; import javax.xml.parsers.*; import org.eclipse.core.runtime.*; import org.eclipse.equinox.internal.p2.core.helpers.LogHelper; import org.eclipse.equinox.internal.p2.core.helpers.Tracing; import org.eclipse.equinox.p2.publisher.eclipse.URLEntry; import org.eclipse.osgi.util.NLS; import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; /** * Parses a category.xml file. * This class was initially copied from org.eclipse.update.core.model.DefaultSiteParser. */ public class CategoryParser extends DefaultHandler { private final static SAXParserFactory parserFactory = SAXParserFactory.newInstance(); private static final String PLUGIN_ID = Activator.ID; private static final String ARCHIVE = "archive"; //$NON-NLS-1$ private static final String CATEGORY = "category"; //$NON-NLS-1$ private static final String CATEGORY_DEF = "category-def"; //$NON-NLS-1$ private static final String DESCRIPTION = "description"; //$NON-NLS-1$ private static final String FEATURE = "feature"; //$NON-NLS-1$ private static final String SITE = "site"; //$NON-NLS-1$ private static final String IU = "iu"; //$NON-NLS-1$ private static final String QUERY = "query"; //$NON-NLS-1$ private static final String EXPRESSION = "expression"; //$NON-NLS-1$ private static final String PARAM = "param"; //$NON-NLS-1$ private static final int STATE_ARCHIVE = 3; private static final int STATE_CATEGORY = 4; private static final int STATE_CATEGORY_DEF = 5; private static final int STATE_DESCRIPTION_CATEGORY_DEF = 7; private static final int STATE_DESCRIPTION_SITE = 6; private static final int STATE_FEATURE = 2; private static final int STATE_IGNORED_ELEMENT = -1; private static final int STATE_INITIAL = 0; private static final int STATE_IU = 8; private static final int STATE_EXPRESSION = 9; private static final int STATE_PARAM = 10; private static final int STATE_QUERY = 11; private static final int STATE_SITE = 1; private boolean DESCRIPTION_SITE_ALREADY_SEEN = false; // Current object stack (used to hold the current object we are // populating in this plugin descriptor Stack<Object> objectStack = new Stack<Object>(); private SAXParser parser; // Current State Information Stack<Integer> stateStack = new Stack<Integer>(); // List of string keys for translated strings private final List<String> messageKeys = new ArrayList<String>(4); private MultiStatus status; /* * */ private static void debug(String s) { Tracing.debug("CategoryParser: " + s); //$NON-NLS-1$ } static void log(Exception e) { LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Internal Error", e)); //$NON-NLS-1$ } static void log(String message) { LogHelper.log(new Status(IStatus.WARNING, Activator.ID, message, null)); } static void log(String message, Exception e) { LogHelper.log(new Status(IStatus.WARNING, Activator.ID, message, e)); } /** * Constructs a site parser. */ public CategoryParser(URI siteLocation) { super(); stateStack = new Stack<Integer>(); objectStack = new Stack<Object>(); status = null; DESCRIPTION_SITE_ALREADY_SEEN = false; try { parserFactory.setNamespaceAware(true); this.parser = parserFactory.newSAXParser(); } catch (ParserConfigurationException e) { log(e); } catch (SAXException e) { log(e); } if (Tracing.DEBUG_GENERATOR_PARSING) debug("Created"); //$NON-NLS-1$ } public int currentState() { Integer state = stateStack.peek(); if (state != null) return state.intValue(); return STATE_IGNORED_ELEMENT; } /** * Handle character text * @see DefaultHandler#characters(char[], int, int) * @since 2.0 */ public void characters(char[] ch, int start, int length) { String text = new String(ch, start, length); //only push if description int state = currentState(); switch (state) { case STATE_DESCRIPTION_SITE : case STATE_DESCRIPTION_CATEGORY_DEF : objectStack.push(text); break; case STATE_EXPRESSION : case STATE_PARAM : text = text.trim(); String existing = null; if (objectStack.peek() instanceof String) existing = (String) objectStack.pop(); if (existing != null) text = existing + text; objectStack.push(text); break; default : break; // nothing } } /** * Handle end of element tags * @see DefaultHandler#endElement(String, String, String) * @since 2.0 */ public void endElement(String uri, String localName, String qName) { String text = null; URLEntry info = null; int state = currentState(); switch (state) { case STATE_IGNORED_ELEMENT : case STATE_ARCHIVE : case STATE_CATEGORY : case STATE_QUERY : stateStack.pop(); break; case STATE_INITIAL : internalError(Messages.DefaultSiteParser_ParsingStackBackToInitialState); break; case STATE_SITE : stateStack.pop(); if (objectStack.peek() instanceof String) { text = (String) objectStack.pop(); SiteModel site = (SiteModel) objectStack.peek(); site.getDescription().setAnnotation(text); } //do not pop the object break; case STATE_FEATURE : stateStack.pop(); objectStack.pop(); break; case STATE_IU : stateStack.pop(); SiteIU completeIU = (SiteIU) objectStack.pop(); String id = completeIU.getID(); String expression = completeIU.getQueryExpression(); if (id == null && expression == null) internalError("The IU must specify an id or an expression to match against."); //$NON-NLS-1$ break; case STATE_EXPRESSION : stateStack.pop(); if (objectStack.peek() instanceof String) { text = (String) objectStack.pop(); SiteIU iu = (SiteIU) objectStack.peek(); iu.setQueryExpression(text); if (Tracing.DEBUG_GENERATOR_PARSING) debug("Found Expression: " + text); //$NON-NLS-1$ } break; case STATE_PARAM : stateStack.pop(); if (objectStack.peek() instanceof String) { text = (String) objectStack.pop(); SiteIU iu = (SiteIU) objectStack.peek(); iu.addQueryParams(text); if (Tracing.DEBUG_GENERATOR_PARSING) debug("Found Param: " + text); //$NON-NLS-1$ } break; case STATE_CATEGORY_DEF : stateStack.pop(); if (objectStack.peek() instanceof String) { text = (String) objectStack.pop(); SiteCategory category = (SiteCategory) objectStack.peek(); category.setDescription(text); } objectStack.pop(); break; case STATE_DESCRIPTION_SITE : stateStack.pop(); text = ""; //$NON-NLS-1$ while (objectStack.peek() instanceof String) { // add text, preserving at most one space between text fragments String newText = (String) objectStack.pop(); if (trailingSpace(newText) && !leadingSpace(text)) { text = " " + text; //$NON-NLS-1$ } text = newText.trim() + text; if (leadingSpace(newText) && !leadingSpace(text)) { text = " " + text; //$NON-NLS-1$ } } text = text.trim(); info = (URLEntry) objectStack.pop(); if (text != null) info.setAnnotation(text); SiteModel siteModel = (SiteModel) objectStack.peek(); // override description. // do not raise error as previous description may be default one // when parsing site tag if (DESCRIPTION_SITE_ALREADY_SEEN) debug(NLS.bind(Messages.DefaultSiteParser_ElementAlreadySet, (new String[] {getState(state)}))); siteModel.setDescription(info); DESCRIPTION_SITE_ALREADY_SEEN = true; break; case STATE_DESCRIPTION_CATEGORY_DEF : stateStack.pop(); text = ""; //$NON-NLS-1$ while (objectStack.peek() instanceof String) { // add text, preserving at most one space between text fragments String newText = (String) objectStack.pop(); if (trailingSpace(newText) && !leadingSpace(text)) { text = " " + text; //$NON-NLS-1$ } text = newText.trim() + text; if (leadingSpace(newText) && !leadingSpace(text)) { text = " " + text; //$NON-NLS-1$ } } text = text.trim(); info = (URLEntry) objectStack.pop(); if (text != null) info.setAnnotation(text); SiteCategory category = (SiteCategory) objectStack.peek(); if (category.getDescription() != null) internalError(NLS.bind(Messages.DefaultSiteParser_ElementAlreadySet, (new String[] {getState(state), category.getLabel()}))); else { checkTranslated(info.getAnnotation()); category.setDescription(info.getAnnotation()); } break; default : internalError(NLS.bind(Messages.DefaultSiteParser_UnknownEndState, (new String[] {getState(state)}))); break; } if (Tracing.DEBUG_GENERATOR_PARSING) debug("End Element:" + uri + ":" + localName + ":" + qName);//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } /* * Handles an error state specified by the status. The collection of all logged status * objects can be accessed using <code>getStatus()</code>. * * @param error a status detailing the error condition */ private void error(IStatus error) { if (status == null) { status = new MultiStatus(PLUGIN_ID, 0, Messages.DefaultSiteParser_ErrorParsingSite, null); } status.add(error); if (Tracing.DEBUG_GENERATOR_PARSING) LogHelper.log(error); } /** * Handle errors * @see DefaultHandler#error(SAXParseException) * @since 2.0 */ public void error(SAXParseException ex) { logStatus(ex); } /** * Handle fatal errors * @see DefaultHandler#fatalError(SAXParseException) * @exception SAXException * @since 2.0 */ public void fatalError(SAXParseException ex) throws SAXException { logStatus(ex); throw ex; } /* * return the state as String */ private String getState(int state) { switch (state) { case STATE_IGNORED_ELEMENT : return "Ignored"; //$NON-NLS-1$ case STATE_INITIAL : return "Initial"; //$NON-NLS-1$ case STATE_SITE : return "Site"; //$NON-NLS-1$ case STATE_FEATURE : return "Feature"; //$NON-NLS-1$ case STATE_IU : return "IU"; //$NON-NLS-1$ case STATE_ARCHIVE : return "Archive"; //$NON-NLS-1$ case STATE_CATEGORY : return "Category"; //$NON-NLS-1$ case STATE_CATEGORY_DEF : return "Category Def"; //$NON-NLS-1$ case STATE_DESCRIPTION_CATEGORY_DEF : return "Description / Category Def"; //$NON-NLS-1$ case STATE_DESCRIPTION_SITE : return "Description / Site"; //$NON-NLS-1$ default : return Messages.DefaultSiteParser_UnknownState; } } /** * Returns all status objects accumulated by the parser. * * @return multi-status containing accumulated status, or <code>null</code>. * @since 2.0 */ public MultiStatus getStatus() { return status; } private void handleCategoryDefState(String elementName, Attributes attributes) { if (elementName.equals(DESCRIPTION)) { stateStack.push(new Integer(STATE_DESCRIPTION_CATEGORY_DEF)); processInfo(attributes); } else internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } private void handleCategoryState(String elementName, Attributes attributes) { internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } private void handleFeatureState(String elementName, Attributes attributes) { if (elementName.equals(CATEGORY)) { stateStack.push(new Integer(STATE_CATEGORY)); processCategory(attributes); } else internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } private void handleInitialState(String elementName, Attributes attributes) throws SAXException { if (elementName.equals(SITE)) { stateStack.push(new Integer(STATE_SITE)); processSite(attributes); } else { internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); // what we received was not a site.xml, no need to continue throw new SAXException(Messages.DefaultSiteParser_InvalidXMLStream); } } private void handleSiteState(String elementName, Attributes attributes) { if (elementName.equals(DESCRIPTION)) { stateStack.push(new Integer(STATE_DESCRIPTION_SITE)); processInfo(attributes); } else if (elementName.equals(FEATURE)) { stateStack.push(new Integer(STATE_FEATURE)); processFeature(attributes); } else if (elementName.equals(IU)) { stateStack.push(new Integer(STATE_IU)); processIU(attributes); } else if (elementName.equals(ARCHIVE)) { stateStack.push(new Integer(STATE_ARCHIVE)); processArchive(attributes); } else if (elementName.equals(CATEGORY_DEF)) { stateStack.push(new Integer(STATE_CATEGORY_DEF)); processCategoryDef(attributes); } else internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } private void handleIUState(String elementName, Attributes attributes) { if (elementName.equals(QUERY)) { stateStack.push(new Integer(STATE_QUERY)); processQuery(attributes); } else if (elementName.equals(CATEGORY)) { stateStack.push(new Integer(STATE_CATEGORY)); processCategory(attributes); } else internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } private void handleQueryState(String elementName, Attributes attributes) { if (elementName.equals(EXPRESSION)) { stateStack.push(new Integer(STATE_EXPRESSION)); processExpression(attributes); } else if (elementName.equals(PARAM)) { stateStack.push(new Integer(STATE_PARAM)); processParam(attributes); } else internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } private void handleExpression(String elementName, Attributes attributes) { internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } private void handleParamState(String elementName, Attributes attributes) { internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {elementName, getState(currentState())}))); } /* * */ private void internalError(String message) { error(new Status(IStatus.ERROR, PLUGIN_ID, IStatus.OK, message, null)); } /* * */ private void internalErrorUnknownTag(String msg) { stateStack.push(new Integer(STATE_IGNORED_ELEMENT)); internalError(msg); } private boolean leadingSpace(String str) { if (str.length() <= 0) { return false; } return Character.isWhitespace(str.charAt(0)); } /* * */ private void logStatus(SAXParseException ex) { String name = ex.getSystemId(); if (name == null) name = ""; //$NON-NLS-1$ else name = name.substring(1 + name.lastIndexOf("/")); //$NON-NLS-1$ String msg; if (name.equals("")) //$NON-NLS-1$ msg = NLS.bind(Messages.DefaultSiteParser_ErrorParsing, (new String[] {ex.getMessage()})); else { String[] values = new String[] {name, Integer.toString(ex.getLineNumber()), Integer.toString(ex.getColumnNumber()), ex.getMessage()}; msg = NLS.bind(Messages.DefaultSiteParser_ErrorlineColumnMessage, values); } error(new Status(IStatus.ERROR, PLUGIN_ID, msg, ex)); } /** * Parses the specified input steam and constructs a site model. * The input stream is not closed as part of this operation. * * @param in input stream * @return site model * @exception SAXException * @exception IOException * @since 2.0 */ public SiteModel parse(InputStream in) throws SAXException, IOException { stateStack.push(new Integer(STATE_INITIAL)); parser.parse(new InputSource(in), this); if (objectStack.isEmpty()) throw new SAXException(Messages.DefaultSiteParser_NoSiteTag); if (objectStack.peek() instanceof SiteModel) { SiteModel site = (SiteModel) objectStack.pop(); site.setMessageKeys(messageKeys); return site; } String stack = ""; //$NON-NLS-1$ Iterator<Object> iter = objectStack.iterator(); while (iter.hasNext()) { stack = stack + iter.next().toString() + "\r\n"; //$NON-NLS-1$ } throw new SAXException(NLS.bind(Messages.DefaultSiteParser_WrongParsingStack, (new String[] {stack}))); } /* * process archive info */ private void processArchive(Attributes attributes) { // don't care about archives in category xml if (Tracing.DEBUG_GENERATOR_PARSING) debug("End processing Archive"); //$NON-NLS-1$ } /* * process the Category info */ private void processCategory(Attributes attributes) { String category = attributes.getValue("name"); //$NON-NLS-1$ Object obj = objectStack.peek(); // TODO could create common class/interface for adding categories if (obj instanceof SiteFeature) ((SiteFeature) obj).addCategoryName(category); else if (obj instanceof SiteIU) ((SiteIU) obj).addCategoryName(category); if (Tracing.DEBUG_GENERATOR_PARSING) debug("End processing Category: name:" + category); //$NON-NLS-1$ } /* * process category def info */ private void processCategoryDef(Attributes attributes) { SiteCategory category = new SiteCategory(); String name = attributes.getValue("name"); //$NON-NLS-1$ String label = attributes.getValue("label"); //$NON-NLS-1$ checkTranslated(label); category.setName(name); category.setLabel(label); SiteModel site = (SiteModel) objectStack.peek(); site.addCategory(category); objectStack.push(category); if (Tracing.DEBUG_GENERATOR_PARSING) debug("End processing CategoryDef: name:" + name + " label:" + label); //$NON-NLS-1$ //$NON-NLS-2$ } /* * process feature info */ private void processFeature(Attributes attributes) { SiteFeature feature = new SiteFeature(); // identifier and version String id = attributes.getValue("id"); //$NON-NLS-1$ String ver = attributes.getValue("version"); //$NON-NLS-1$ boolean noId = (id == null || id.trim().equals("")); //$NON-NLS-1$ // We need to have id and version, or the url, or both. if (noId) internalError(NLS.bind(Messages.DefaultSiteParser_Missing, (new String[] {"url", getState(currentState())}))); //$NON-NLS-1$ feature.setFeatureIdentifier(id); feature.setFeatureVersion(ver); SiteModel site = (SiteModel) objectStack.peek(); site.addFeature(feature); feature.setSiteModel(site); objectStack.push(feature); if (Tracing.DEBUG_GENERATOR_PARSING) debug("End Processing Feature Tag: id:" + id + " version:" + ver); //$NON-NLS-1$ //$NON-NLS-2$ } /* * process IU info */ private void processIU(Attributes attributes) { SiteIU iu = new SiteIU(); SiteModel site = (SiteModel) objectStack.peek(); // identifier and version String id = attributes.getValue("id"); //$NON-NLS-1$ String range = attributes.getValue("range"); //$NON-NLS-1$ id = id == null ? null : id.trim(); range = range == null ? null : range.trim(); iu.setID(id); iu.setRange(range); site.addIU(iu); objectStack.push(iu); if (Tracing.DEBUG_GENERATOR_PARSING) debug("End processing iu."); //$NON-NLS-1$ } /* * process expression info */ private void processExpression(Attributes attributes) { SiteIU iu = (SiteIU) objectStack.peek(); iu.setQueryType(attributes.getValue("type")); //$NON-NLS-1$ if (Tracing.DEBUG_GENERATOR_PARSING) debug("End processing Expression: " + iu.getQueryType()); //$NON-NLS-1$ } /* * process query info */ private void processQuery(Attributes attributes) { // TODO may have simple attriutes for id and range if (Tracing.DEBUG_GENERATOR_PARSING) debug("End processing Query."); //$NON-NLS-1$ } /* * process param info */ private void processParam(Attributes attributes) { if (Tracing.DEBUG_GENERATOR_PARSING) debug("End processing Param."); //$NON-NLS-1$ } /* * process URL info with element text */ private void processInfo(Attributes attributes) { URLEntry inf = new URLEntry(); String infoURL = attributes.getValue("url"); //$NON-NLS-1$ inf.setURL(infoURL); if (Tracing.DEBUG_GENERATOR_PARSING) debug("Processed Info: url:" + infoURL); //$NON-NLS-1$ objectStack.push(inf); } /* * process site info */ private void processSite(Attributes attributes) { // create site map SiteModel site = new SiteModel(); objectStack.push(site); if (Tracing.DEBUG_GENERATOR_PARSING) debug("End process Site tag."); //$NON-NLS-1$ } /** * Handle start of element tags * @see DefaultHandler#startElement(String, String, String, Attributes) * @since 2.0 */ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (Tracing.DEBUG_GENERATOR_PARSING) { debug("State: " + currentState()); //$NON-NLS-1$ debug("Start Element: uri:" + uri + " local Name:" + localName + " qName:" + qName);//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } switch (currentState()) { case STATE_IGNORED_ELEMENT : internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownElement, (new String[] {localName, getState(currentState())}))); break; case STATE_INITIAL : handleInitialState(localName, attributes); break; case STATE_SITE : handleSiteState(localName, attributes); break; case STATE_FEATURE : handleFeatureState(localName, attributes); break; case STATE_IU : handleIUState(localName, attributes); break; case STATE_EXPRESSION : handleExpression(localName, attributes); break; case STATE_QUERY : handleQueryState(localName, attributes); break; case STATE_PARAM : handleParamState(localName, attributes); break; case STATE_ARCHIVE : handleSiteState(localName, attributes); break; case STATE_CATEGORY : handleCategoryState(localName, attributes); break; case STATE_CATEGORY_DEF : handleCategoryDefState(localName, attributes); break; case STATE_DESCRIPTION_SITE : handleSiteState(localName, attributes); break; case STATE_DESCRIPTION_CATEGORY_DEF : handleSiteState(localName, attributes); break; default : internalErrorUnknownTag(NLS.bind(Messages.DefaultSiteParser_UnknownStartState, (new String[] {getState(currentState())}))); break; } } private boolean trailingSpace(String str) { if (str.length() <= 0) { return false; } return Character.isWhitespace(str.charAt(str.length() - 1)); } // Add translatable strings from the site.xml // to the list of message keys. private void checkTranslated(String value) { if (value != null && value.length() > 1 && value.startsWith("%")) //$NON-NLS-1$ messageKeys.add(value.substring(1)); } }