/** * This file Copyright (c) 2005-2008 Aptana, Inc. This program is * dual-licensed under both the Aptana Public License and the GNU General * Public license. You may elect to use one or the other of these licenses. * * This program is distributed in the hope that it will be useful, but * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or * NONINFRINGEMENT. Redistribution, except as permitted by whichever of * the GPL or APL you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or modify this * program under the terms of the GNU General Public License, * Version 3, as published by the Free Software Foundation. You should * have received a copy of the GNU General Public License, Version 3 along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Aptana provides a special exception to allow redistribution of this file * with certain other free and open source software ("FOSS") code and certain additional terms * pursuant to Section 7 of the GPL. You may view the exception and these * terms on the web at http://www.aptana.com/legal/gpl/. * * 2. For the Aptana Public License (APL), this program and the * accompanying materials are made available under the terms of the APL * v1.0 which accompanies this distribution, and is available at * http://www.aptana.com/legal/apl/. * * You may view the GPL, Aptana's exception and additional terms, and the * APL in the file titled license.html at the root of the corresponding * plugin containing this source file. * * Any modifications to this file must keep this entire header intact. */ package com.aptana.xml; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.eclipse.core.runtime.Platform; import org.osgi.framework.Bundle; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * @author Kevin Lindsey */ public class Parser extends DefaultHandler { private static final Map<Class<?>,Class<?>> PRIMITIVE_TO_CLASS = new HashMap<Class<?>,Class<?>>(); private static final Pattern PARSE_ERROR_LINE_NUMBER = Pattern.compile(" line (\\d+)"); //$NON-NLS-1$ /** * static constructor */ static { PRIMITIVE_TO_CLASS.put(boolean.class, Boolean.class); PRIMITIVE_TO_CLASS.put(char.class, Character.class); PRIMITIVE_TO_CLASS.put(int.class, Integer.class); PRIMITIVE_TO_CLASS.put(long.class, Long.class); PRIMITIVE_TO_CLASS.put(float.class, Float.class); PRIMITIVE_TO_CLASS.put(double.class, Double.class); } private String _namespace; private List<String> _packages; private List<String> _suffixes; private Map<Class<?>,IConverter> _convertersByClass; private Map<String,Class<?>> _classByName; private INode _currentNode; private Stack<INode> _nodes; private DocumentNode _documentNode; private StringBuffer _textBuffer; private Locator _locator; private Class<?> _unknownElementClass; private BundleClassLoader _classLoader; private IErrorHandler _errorHandler; private boolean _cacheClasses; /** * Parser */ public Parser() { this(null); } /** * Parser * * @param namespace */ public Parser(String namespace) { this._namespace = namespace; this._packages = new ArrayList<String>(); this._suffixes = new ArrayList<String>(); this._convertersByClass = new HashMap<Class<?>,IConverter>(); this._classByName = new HashMap<String,Class<?>>(); this._nodes = new Stack<INode>(); this._textBuffer = new StringBuffer(); this.addConverters(); this.addPackages(); this.addSuffixes(); this._classLoader = new BundleClassLoader(); //this.addBundle(Platform.getBundle("com.aptana.ide.lexer")); //$NON-NLS-1$ this._unknownElementClass = NodeBase.class; this._cacheClasses = true; } /** * addBundle * * @param bundle */ public void addBundle(Bundle bundle) { if (bundle != null) { this._classLoader.addBundle(bundle); } } /** * addClass * * @param name * @param type */ public void addClass(String name, Class<?> type) { this._classByName.put(name, type); } /** * addConverter * * @param targetType * @param converter */ public void addConverter(Class<?> targetType, IConverter converter) { this._convertersByClass.put(targetType, converter); } /** * addConverters */ protected void addConverters() { this.addConverter(Boolean.class, new BooleanConverter()); this.addConverter(Character.class, new CharacterConverter()); this.addConverter(Double.class, new DoubleConverter()); this.addConverter(Float.class, new FloatConverter()); this.addConverter(Integer.class, new IntegerConverter()); this.addConverter(boolean.class, new BooleanConverter()); this.addConverter(char.class, new CharacterConverter()); this.addConverter(double.class, new DoubleConverter()); this.addConverter(float.class, new FloatConverter()); this.addConverter(int.class, new IntegerConverter()); } /** * addPackage * * @param packageName */ public void addPackage(String packageName) { if (this._packages.contains(packageName) == false) { this._packages.add(packageName); } } /** * addPackages */ protected void addPackages() { java.lang.Package thisPackage = this.getClass().getPackage(); String packageName = thisPackage.getName(); this.addPackage(packageName); } /** * addSuffix * * @param suffix */ public void addSuffix(String suffix) { if (this._suffixes.contains(suffix) == false) { this._suffixes.add(suffix); } } /** * addSuffixes */ protected void addSuffixes() { // this causes the mapped class name (from toClassName) to be tested first this.addSuffix(""); //$NON-NLS-1$ } /** * appendText */ private void appendText() { if (this._currentNode != null) { this._currentNode.appendText(this._textBuffer.toString()); } this._textBuffer.setLength(0); } /** * applyAttributes * * @param node * @param attributes */ private void applyAttributes(INode node, Attributes attributes) { for (int i = 0; i < attributes.getLength(); i++) { String name = attributes.getLocalName(i); String value = attributes.getValue(i); Method setter = this.findSetter(node, name); if (setter != null) { Class<?> parameterType = setter.getParameterTypes()[0]; // map primitives to their "boxed" class types if (parameterType.isPrimitive() && PRIMITIVE_TO_CLASS.containsKey(parameterType)) { parameterType = PRIMITIVE_TO_CLASS.get(parameterType); } if (parameterType == String.class) { try { setter.invoke(node, new Object[] { value }); } catch (Exception e) { String message = MessageFormat.format( Messages.Parser_Error_Invoking_Setter, new Object[] { setter.getName(), name } ); this.sendError(message); } } else { // get converter IConverter converter = this.findConverter(parameterType); if (converter != null) { // create argument array Object ary = Array.newInstance(parameterType, 1); // set value in argument array Array.set(ary, 0, converter.fromString(value)); try { Object[] argsArray = (Object[]) ary; setter.invoke(node, argsArray); } catch (Exception e) { String message = MessageFormat.format( Messages.Parser_Error_Invoking_Setter, new Object[] { setter.getName(), name } ); this.sendError(message); } } else { String message = MessageFormat.format( Messages.Parser_No_Converter, new Object[] { parameterType.getName() } ); this.sendError(message); } } } else { // TODO: add setProperty(name,value) to INode and call that method here String message = MessageFormat.format( Messages.Parser_No_Setter, new Object[] { name } ); this.sendWarning(message); } } } /** * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int) */ public void characters(char[] ch, int start, int length) throws SAXException { this._textBuffer.append(ch, start, length); } /** * createDocumentNode * * @return DocumentNode */ protected DocumentNode createDocumentNode() { return new DocumentNode(); } /** * @see org.xml.sax.helpers.DefaultHandler#endDocument() */ public void endDocument() throws SAXException { this._currentNode = null; this._nodes.clear(); this._textBuffer.setLength(0); } /** * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String) */ public void endElement(String uri, String localName, String qName) throws SAXException { this.appendText(); if (this._namespace == null || this._namespace.equals(uri)) { if (localName.indexOf('.') == -1) { this.exitObject(); } else { this.setProperty(localName); } } } /** * enterMatcher * * @param name * @param attributes */ private void enterObject(String name, Attributes attributes) throws Exception { // get class for this element String className = this.toClassName(name); // NOTE: findClass never returns null Class<?> nodeClass = this.findClass(className); this.appendText(); try { Constructor<?> ctor = nodeClass.getConstructor(new Class[0]); INode node = (INode) ctor.newInstance(new Object[0]); node.setLineNumber(this._locator.getLineNumber()); node.setColumnNumber(this._locator.getColumnNumber()); // set properties this.applyAttributes(node, attributes); if (this._currentNode != null) { // add node to stack this._nodes.push(this._currentNode); } // update current node; this._currentNode = node; } catch (Exception e) { String message = MessageFormat.format( Messages.Parser_Could_Not_Create_Class, new Object[] { nodeClass.getName(), name } ); this.sendError(message); } } /** * exitMatcher */ private void exitObject() { if (this._nodes.size() > 0) { // special processing for Package Element if (this._currentNode instanceof Package) { String packageName = this._currentNode.getText(); this.addPackage(packageName); } else if (this._currentNode instanceof com.aptana.xml.Bundle) { String bundleName = this._currentNode.getText(); Bundle bundle = Platform.getBundle(bundleName); if (bundle != null) { this.addBundle(bundle); } else { this.sendWarning(Messages.Parser_Bundle_Not_Found + bundleName); } } // remove parent from stack INode parent = this._nodes.pop(); try { if (this._currentNode != null) { // add current node to parent parent.appendChild(this._currentNode); } } catch (IllegalArgumentException e) { this.sendError(e.getMessage()); } // set parent as current node this._currentNode = parent; } else { // NOTE: this should never happen this._currentNode = null; } } /** * findClass * * @param className * @return Class */ private Class<?> findClass(String className) { Class<?> result; if (this._classByName.containsKey(className) == false) { Class<?> candidate = null; // loop through the packages to see if we can find our class for (int i = 0; i < this._packages.size(); i++) { String pkg = this._packages.get(i); String fullClassName = pkg + "." + className; //$NON-NLS-1$ for (int j = 0; j < this._suffixes.size(); j++) { String suffix = this._suffixes.get(j); String extendedName = fullClassName + suffix; try { candidate = this._classLoader.loadClass(extendedName); } catch (ClassNotFoundException e) { // ignore class not founds } if (candidate != null) { if (INode.class.isAssignableFrom(candidate)) { break; } else { candidate = null; } // TODO: may want to add info stating that a class was skipped // since it wasn't type compatible with INode } } // exit if we found a class if (candidate != null) { break; } } // fallback to generic node if we didn't find anything if (candidate == null) { candidate = this._unknownElementClass; String message = MessageFormat.format( Messages.Parser_Class_Not_Found_Using_Replacement, new Object[] { className, this._unknownElementClass.getName() } ); this.sendError(message); } // cache results to avoid future lookups if (this._cacheClasses) { this.addClass(className, candidate); } result = candidate; } else { result = this._classByName.get(className); } return result; } /** * findConverter * * @param parameterType * @return IConverter */ private IConverter findConverter(Class<?> parameterType) { if (this._convertersByClass.containsKey(parameterType) == false) { Class<?> candidate = null; String converterName = parameterType.getName() + "Converter"; //$NON-NLS-1$ IConverter instance = null; // loop through the packages to see if we can find our class for (int i = 0; i < this._packages.size(); i++) { String pkg = this._packages.get(i); String fullConverterName = pkg + "." + converterName; //$NON-NLS-1$ try { candidate = this._classLoader.loadClass(fullConverterName); } catch (ClassNotFoundException e) { // ignore class not founds } if (candidate != null) { break; } } // create an instance, if we found a class if (candidate != null) { try { Constructor<?> ctor = candidate.getConstructor(new Class[0]); Object converter = ctor.newInstance(new Object[0]); // use this instance if it's an IConverter if (converter instanceof IConverter) { instance = (IConverter) converter; } } catch (Exception e) { // e.printStackTrace(); } } this.addConverter(parameterType, instance); } return this._convertersByClass.get(parameterType); } /** * findSetter * * @param object * @param name * @return Method or null */ private Method findSetter(Object object, String name) { Method result = null; String methodName = this.toMethodName(name); Method[] methods = object.getClass().getMethods(); List<Method> candidates = new ArrayList<Method>(); for (int i = 0; i < methods.length; i++) { Method method = methods[i]; Class<?>[] parameterTypes = method.getParameterTypes(); if (method.getName().equals(methodName) && parameterTypes.length == 1) { if (parameterTypes[0] == String.class) { // prefer a String arg over all other types candidates.add(0, method); } else { candidates.add(method); } } } if (candidates.size() > 0) { result = candidates.get(0); } // TODO: cache results return result; } /** * cacheClasses * * @return boolean */ public boolean cacheClasses() { return this._cacheClasses; } /** * getClassLoader * * @return ClassLoader */ public ClassLoader getClassLoader() { return this._classLoader; } /** * getUnknownElementClass * * @return Class */ public Class<?> getUnknownElementClass() { return this._unknownElementClass; } /** * Load the specified binary grammar file * * @param file * @return Object or null */ public DocumentNode loadXML(File file) { DocumentNode result = null; try { FileInputStream inputStream = new FileInputStream(file); result = this.loadXML(inputStream); } catch (FileNotFoundException e) { // e.printStackTrace(); } return result; } /** * load * * @param in * @return DocumentNode */ public DocumentNode loadXML(InputStream in) { try { // create a new SAX factory class SAXParserFactory factory = SAXParserFactory.newInstance(); // make sure it generates namespace aware parsers factory.setNamespaceAware(true); // create the parser SAXParser saxParser = factory.newSAXParser(); // parse the XML saxParser.parse(in, this); } catch (Exception e) { this.sendError(e.getMessage()); } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } return this._documentNode; } /** * removeBundle * * @param bundle */ public void removeBundle(Bundle bundle) { if (bundle != null) { this._classLoader.removeBundle(bundle); } } /** * removeClass * * @param name */ public void removeClass(String name) { this._classByName.remove(name); } /** * removeConverter * * @param targetType */ public void removeConverter(Class<?> targetType) { this._convertersByClass.remove(targetType); } /** * removePackage * * @param packageName */ public void removePackage(String packageName) { this._packages.remove(packageName); } /** * removeSuffix * * @param suffix */ public void removeSuffix(String suffix) { this._suffixes.remove(suffix); } /** * sendError * * @param message */ private void sendError(String message) { if (this._errorHandler != null) { int line = this._locator.getLineNumber(); int column = this._locator.getColumnNumber(); if (line == -1) { Matcher m = PARSE_ERROR_LINE_NUMBER.matcher(message); if (m.find()) { line = Integer.parseInt(m.group(1)); } else { line = 1; } } if (column == -1) { column = 0; } this._errorHandler.handleError(line, column, message); } } // /** // * sendInfo // * // * @param message // */ // private void sendInfo(String message) // { // if (this._errorHandler != null) // { // int line = this._locator.getLineNumber(); // int column = this._locator.getColumnNumber(); // // this._errorHandler.handleInfo(line, column, message); // } // } /** * sendWarning * * @param message */ private void sendWarning(String message) { if (this._errorHandler != null) { int line = this._locator.getLineNumber(); int column = this._locator.getColumnNumber(); if (line == -1) { Matcher m = PARSE_ERROR_LINE_NUMBER.matcher(message); if (m.find()) { line = Integer.parseInt(m.group(1)); } else { line = 1; } } if (column == -1) { column = 0; } this._errorHandler.handleWarning(line, column, message); } } /** * setCacheClasses * * @param value */ public void setCacheClasses(boolean value) { this._cacheClasses = value; } /** * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator) */ public void setDocumentLocator(Locator locator) { super.setDocumentLocator(locator); this._locator = locator; } /** * setErrorHandler * * @param errorHandler */ public void setErrorHandler(IErrorHandler errorHandler) { this._errorHandler = errorHandler; } /** * setProperty * * @param localName * @param dotIndex */ private void setProperty(String localName) { int dotIndex = localName.indexOf('.'); if (this._nodes.size() > 0 && this._currentNode.getChildCount() > 0) { INode parent = this._nodes.pop(); INode value = this._currentNode.getChild(0); if (dotIndex < localName.length() - 1) { String name = localName.substring(dotIndex + 1); String methodName = this.toMethodName(name); Method[] methods = parent.getClass().getMethods(); for (int i = 0; i < methods.length; i++) { Method method = methods[i]; if (methodName.equals(method.getName())) { Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 1) { Class<?> parameterType = parameterTypes[0]; if (parameterType.isInstance(value)) { // create argument array Object ary = Array.newInstance(parameterType, 1); // set value in argument array Array.set(ary, 0, value); try { method.invoke(parent, (Object[]) ary); break; } catch (Exception e) { // e.printStackTrace(); } break; } } } } } this._currentNode = parent; } else { // this should not happen this._currentNode = null; } } /** * setUnknownElementClass * * @param elementClass */ public void setUnknownElementClass(Class<?> elementClass) { if (elementClass == null || INode.class.isInstance(elementClass) == false) { this._unknownElementClass = NodeBase.class; } else { this._unknownElementClass = elementClass; } } /** * @see org.xml.sax.helpers.DefaultHandler#startDocument() */ public void startDocument() throws SAXException { this._documentNode = this.createDocumentNode(); this._currentNode = this._documentNode; this._documentNode.setErrorHandler(this._errorHandler); } /** * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, * org.xml.sax.Attributes) */ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (this._namespace == null || this._namespace.equals(uri)) { try { if (localName.indexOf('.') == -1) { this.enterObject(localName, attributes); } else { if (this._currentNode != null) { // add node to stack this._nodes.push(this._currentNode); } // update current node; this._currentNode = new Property(); } } catch (Exception e) { throw new SAXException(e); } } } /** * convertName * * @param name * @return String */ private String toClassName(String name) { StringBuffer sb = new StringBuffer(); boolean toUpper = true; for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); if (c == '-') { toUpper = true; } else { if (toUpper) { // add uppercase version of current letter sb.append(Character.toUpperCase(c)); // reset flag toUpper = false; } else { // add current letter sb.append(c); } } } return sb.toString(); } /** * toMethodName * * @param name * @return String */ private String toMethodName(String name) { StringBuffer sb = new StringBuffer(); boolean toUpper = true; // prepend "set" sb.append("set"); //$NON-NLS-1$ for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); if (c == '-') { toUpper = true; } else { if (toUpper) { // add uppercase version of current letter sb.append(Character.toUpperCase(c)); // reset flag toUpper = false; } else { // add current letter sb.append(c); } } } return sb.toString(); } }