# HG changeset patch # User Omair Majid # Date 1368463197 14400 # Node ID bf4f8d4e38a9f5a0a717c9acc7d0267261cc7796 # Parent 066ee2d7317c42f1857b4a63bba822831db7d5e7# Parent 0403e2a4e430e4e6c893cf51649b8b0f1469c40a Merge diff -r 066ee2d7317c -r bf4f8d4e38a9 host-cpu/distribution/plugin.xml --- a/host-cpu/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/host-cpu/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 host-memory/distribution/plugin.xml --- a/host-memory/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/host-memory/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 host-overview/distribution/plugin.xml --- a/host-overview/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/host-overview/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java Tue May 07 18:27:02 2013 -0400 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java Mon May 13 12:39:57 2013 -0400 @@ -92,13 +92,18 @@ File configurationFile = new File(pluginDir, PLUGIN_CONFIG_FILE); PluginConfiguration pluginConfig = parser.parse(configurationFile); loadNewAndExtendedCommands(internalJarRoot, pluginDir, pluginConfig); - } catch (PluginConfigurationParseException | FileNotFoundException exception) { + } catch (PluginConfigurationParseException exception) { logger.log(Level.WARNING, "unable to parse plugin configuration", exception); + } catch (PluginConfigurationValidatorException exception) { + logger.log(Level.WARNING, "unable to validate " + exception.getFilePath() + " file\n"); + } catch (FileNotFoundException exception) { + logger.log(Level.WARNING, "file not found", exception); } } - combineCommands(); } + + private void loadNewAndExtendedCommands(File coreJarRoot, File pluginDir, PluginConfiguration pluginConfig) { @@ -203,5 +208,6 @@ } return result; } + } diff -r 066ee2d7317c -r bf4f8d4e38a9 launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java --- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java Tue May 07 18:27:02 2013 -0400 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java Mon May 13 12:39:57 2013 -0400 @@ -36,11 +36,15 @@ package com.redhat.thermostat.launcher.internal; + +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -50,6 +54,11 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import javax.xml.XMLConstants; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; @@ -61,7 +70,6 @@ import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; -import com.redhat.thermostat.common.Pair; import com.redhat.thermostat.common.utils.LoggingUtils; import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandExtensions; import com.redhat.thermostat.launcher.internal.PluginConfiguration.NewCommand; @@ -188,16 +196,32 @@ * This class is thread-safe */ public class PluginConfigurationParser { - + private static final Logger logger = LoggingUtils.getLogger(PluginConfigurationParser.class); - // thread safe because there is no state :) - - public PluginConfiguration parse(File configurationFile) throws FileNotFoundException { + + public PluginConfiguration parse(File configurationFile) throws FileNotFoundException, PluginConfigurationValidatorException { + validate(new StreamSource(configurationFile)); return parse(configurationFile.getParentFile().getName(), new FileInputStream(configurationFile)); } - - public PluginConfiguration parse(String pluginName, InputStream configurationStream) { + + void validate(StreamSource plugin) throws PluginConfigurationValidatorException { + URL schemaUrl = getClass().getResource("/plugin.xsd"); + SchemaFactory schemaFactory = + SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + try { + Schema schema = schemaFactory.newSchema(schemaUrl); + Validator validator = schema.newValidator(); + validator.setErrorHandler(new ConfigurationValidatorErrorHandler()); + validator.validate(plugin); + } catch (IOException | SAXException exception) { + throw new PluginConfigurationValidatorException + (plugin.getSystemId(), exception.getLocalizedMessage(), exception); + } + } + + PluginConfiguration parse(String pluginName, InputStream configurationStream) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setIgnoringComments(true); @@ -208,12 +232,12 @@ if (rootNode == null) { throw new PluginConfigurationParseException("no configuration found"); } - return parseRootElement(pluginName, rootNode); + return parseRootElement(pluginName, rootNode); } catch (ParserConfigurationException | SAXException | IOException exception) { throw new PluginConfigurationParseException("failed to parse plugin configuration", exception); } } - + private PluginConfiguration parseRootElement(String pluginName, Node root) { List commands = Collections.emptyList(); List extensions = Collections.emptyList(); @@ -505,4 +529,85 @@ throw exception; } } + + private static class ConfigurationValidatorErrorHandler implements ErrorHandler { + + private int warningsErrorsCounter = 0; + + @Override + public void warning(SAXParseException exception) throws SAXException { + warningsErrorsCounter++; + printInfo(exception, "Warning"); + } + + @Override + public void error(SAXParseException exception) throws SAXParseException { + warningsErrorsCounter++; + printInfo(exception, "Error"); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXParseException { + if (warningsErrorsCounter == 0) { + printInfo(exception, "Fatal error"); + logger.warning("XML not well formed"); + } + } + + private static void printInfo(SAXParseException e, String errorType) { + int columnNumber = e.getColumnNumber(); + int lineNumber = e.getLineNumber(); + + StringBuffer buffer = new StringBuffer(); + + String firstLine = null; + String secondLine = null; + String thirdLine = null; + String errorLine = null; + String pointer = ""; + String absolutePath = e.getSystemId(); + absolutePath = absolutePath.substring(5); + + try { + BufferedReader br = new BufferedReader(new FileReader(absolutePath)); + for (int i = 1; i < lineNumber-3; i++) { + br.readLine(); + } + firstLine = br.readLine(); + secondLine = br.readLine(); + thirdLine = br.readLine(); + errorLine = br.readLine(); + + for (int j = 1; j < columnNumber-1; j++) { + pointer = pointer.concat(" "); + } + pointer = pointer.concat("^"); + br.close(); + } catch (IOException exception) { + System.out.println("File not found!");; + } + + buffer.append(errorType + " in file " + absolutePath + ":" + lineNumber + "." + columnNumber + "\n"); + buffer.append(formatMessage(e.getLocalizedMessage()) + "\n\n"); + buffer.append(firstLine + "\n"); + buffer.append(secondLine + "\n"); + buffer.append(thirdLine + "\n"); + buffer.append(errorLine + "\n"); + buffer.append(pointer + "\n"); + + logger.warning("\n" + buffer.toString()); + + } + + private static String formatMessage(String message) { + String[] arguments = message.split("\"http://icedtea.classpath.org/thermostat/plugins/v1.0\":"); + int size = arguments.length; + String output = ""; + for (int i = 0; i < size; i++) { + output=output.concat(arguments[i]); + } + return output; + } + } + } diff -r 066ee2d7317c -r bf4f8d4e38a9 launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationValidatorException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationValidatorException.java Mon May 13 12:39:57 2013 -0400 @@ -0,0 +1,79 @@ +/* + * Copyright 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.launcher.internal; + + +public class PluginConfigurationValidatorException extends Exception { + + private static final long serialVersionUID = 1L; + private String filePath; + + /** + * Constructor of PluginConfigurationValidatorException + * @param filePath must include the protocol + * @param message the detailed message + */ + public PluginConfigurationValidatorException(String filePath, String message) { + super(message); + this.filePath = computeFilePath(filePath); + } + + /** + * Constructor of PluginConfigurationValidatorException + * @param filePath must include the protocol + * @param message the detailed message + */ + public PluginConfigurationValidatorException(String filePath, String message, Throwable cause) { + super(message, cause); + this.filePath = computeFilePath(filePath); + } + + public String getFilePath() { + return filePath; + } + + /** + * Computes the file path removing the protocol scheme + * @param filePath must include the protocol + * @return the path without the protocol scheme + */ + private String computeFilePath(String filePath) { + // the substring starts from position 5, skipping "file:" filePath content + return filePath.substring(5); + } + +} diff -r 066ee2d7317c -r bf4f8d4e38a9 launcher/src/main/resources/plugin.xsd --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/launcher/src/main/resources/plugin.xsd Mon May 13 12:39:57 2013 -0400 @@ -0,0 +1,1 @@ +../../../../distribution/docs/plugin.xsd \ No newline at end of file diff -r 066ee2d7317c -r bf4f8d4e38a9 launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java Tue May 07 18:27:02 2013 -0400 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java Mon May 13 12:39:57 2013 -0400 @@ -74,7 +74,7 @@ private UsageStringBuilder usageBuilder; @Before - public void setUp() throws IOException { + public void setUp() throws IOException, PluginConfigurationValidatorException { parser = mock(PluginConfigurationParser.class); parserResult = mock(PluginConfiguration.class); when(parser.parse(isA(File.class))).thenReturn(parserResult); @@ -108,7 +108,7 @@ } @Test - public void verifyParserIsInvokedOnAllConfigurationFiles() throws IOException { + public void verifyParserIsInvokedOnAllConfigurationFiles() throws IOException, PluginConfigurationValidatorException { Path[] pluginDirs = new Path[] { pluginRootDir.resolve("plugin1"), pluginRootDir.resolve("plugin2"), @@ -130,7 +130,7 @@ } @Test - public void verifyMissingConfigurationFileIsHandledCorrectly() throws FileNotFoundException { + public void verifyMissingConfigurationFileIsHandledCorrectly() throws FileNotFoundException, PluginConfigurationValidatorException { when(parser.parse(isA(File.class))).thenThrow(new FileNotFoundException("test")); new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser, usageBuilder); diff -r 066ee2d7317c -r bf4f8d4e38a9 launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java --- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java Tue May 07 18:27:02 2013 -0400 +++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java Mon May 13 12:39:57 2013 -0400 @@ -44,11 +44,9 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import java.io.ByteArrayInputStream; -import java.io.UnsupportedEncodingException; -import java.util.Arrays; -import java.util.List; - +import java.io.*; +import java.util.*; +import javax.xml.transform.stream.StreamSource; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; @@ -67,13 +65,30 @@ parser.parse("test", new ByteArrayInputStream(config.getBytes("UTF-8"))); fail("should not reach here"); } + + @Test + public void validateEmptyConfiguration() throws IOException { + String config = "\n"; + PluginConfigurationParser parser = new PluginConfigurationParser(); + File testFile = createFile("testSystemId", config); + try { + parser.validate(new StreamSource(testFile)); + fail("should not come here"); + } catch (PluginConfigurationValidatorException e) { + //pass + } finally { + testFile.delete(); + } + } @Test public void testMinimalConfiguration() throws UnsupportedEncodingException { PluginConfigurationParser parser = new PluginConfigurationParser(); String config = "" + "\n" + - "\n" + + "\n" + ""; PluginConfiguration result = parser.parse("test", new ByteArrayInputStream(config.getBytes("UTF-8"))); @@ -84,7 +99,9 @@ @Test public void testConfigurationThatExtendsExistingCommand() throws UnsupportedEncodingException { String config = "\n" + - "\n" + + "\n" + " \n" + " \n" + " test\n" + @@ -113,11 +130,100 @@ assertEquals(Arrays.asList("foo", "bar", "baz"), first.getPluginBundles()); assertEquals(Arrays.asList("thermostat-foo"), first.getDepenedencyBundles()); } + + @Test + public void canValidatePluginXMLMultipleTimes() throws Exception { + + try { + String config = "\n" + + "\n" + + " \n" + + " \n" + + " test\n" + + " \n" + + " foo\n" + + " bar\n" + + " baz\n" + + " \n" + + " \n" + + " thermostat-foo\n" + + " \n" + + " \n" + + " \n" + + ""; + PluginConfigurationParser parser = new PluginConfigurationParser(); + File testFile = createFile("testSystemId", config); + parser.validate(new StreamSource(testFile)); + testFile.delete(); + + config = "\n" + + "\n" + + " \n" + + " \n" + + " test\n" + + " \n" + + " foo\n" + + " bar\n" + + " baz\n" + + " \n" + + " \n" + + " thermostat-foo\n" + + " \n" + + " \n" + + " \n" + + ""; + File testFile2 = createFile("testSystemId", config); + parser.validate(new StreamSource(testFile)); + testFile2.delete(); + } catch (PluginConfigurationValidatorException e) { + fail("should not reach here, plugin.xml should be validated according to schema"); + } + } + + @Test + public void validationFailsOnInvalidPluginFile() throws Exception { + String config = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " test\n" + + " \n" + + " foo\n" + + " bar\n" + + " baz\n" + + " \n" + + " \n" + + " thermostat-foo\n" + + " \n" + + " \n" + + " \n" + + ""; + + PluginConfigurationParser parser = new PluginConfigurationParser(); + File testFile = createFile("testSystemId", config); + try { + parser.validate(new StreamSource(testFile)); + fail("plugin.xml should not validate according to schema"); + } catch (PluginConfigurationValidatorException e) { + //pass + } finally { + testFile.delete(); + } + } @Test public void testConfigurationThatAddsNewCommand() throws UnsupportedEncodingException { String config = "\n" + - "\n" + + "\n" + " \n" + " \n" + " test\n" + @@ -156,7 +262,9 @@ @Test public void testSpacesAtStartAndEndAreTrimmed() throws UnsupportedEncodingException { String config = "\n" + - "\n" + + "\n" + " " + " \n" + " \ntest \n\n" + @@ -222,9 +330,70 @@ } @Test + public void canValidateCorrectFile() throws IOException { + String config = "\n" + + "\n" + + " \n" + + " \n" + + " test\n" + + " just a test\n" + + " \n" + + " \n" + + " true\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n \t \nfoo\t \n \n\n" + + " \tbar baz\n\n" + + " buzz\n" + + " \n" + + " \n\t\n\t \t\t\n" + + " \t\t\t thermostat-foo\n\t\t\n\n" + + " \n" + + " \n" + + " \n" + + ""; + + PluginConfigurationParser parser = new PluginConfigurationParser(); + File testFile = createFile("testSystemId", config); + try { + parser.validate(new StreamSource(testFile)); + } catch (PluginConfigurationValidatorException e) { + fail("should not reach here, plugin.xml should be validated according to schema"); + } finally { + testFile.delete(); + } + + } + @Test public void testOptionParsing() throws UnsupportedEncodingException { String config = "\n" + - "\n" + + "\n" + " \n" + " \n" + " test\n" + @@ -295,7 +464,9 @@ @Test public void testCommonOptionParsing() throws UnsupportedEncodingException { String config = "\n" + - "\n" + + "\n" + " \n" + " \n" + " test\n" + @@ -339,7 +510,9 @@ @Test public void testFakeCommonOptionIsIgnored() throws UnsupportedEncodingException { String config = "\n" + - "\n" + + "\n" + " \n" + " \n" + " test\n" + @@ -369,4 +542,12 @@ Option dbUrlOption = opts.getOption("foobarbaz"); assertNull(dbUrlOption); } + + private File createFile(String fileName, String contents) throws IOException { + FileWriter fstream = new FileWriter(fileName); + BufferedWriter out = new BufferedWriter(fstream); + out.write(contents); + out.close(); + return new File(fileName); + } } diff -r 066ee2d7317c -r bf4f8d4e38a9 numa/distribution/plugin.xml --- a/numa/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/numa/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 thread/distribution/plugin.xml --- a/thread/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/thread/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 vm-classstat/distribution/plugin.xml --- a/vm-classstat/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/vm-classstat/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 vm-cpu/distribution/plugin.xml --- a/vm-cpu/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/vm-cpu/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 vm-gc/distribution/plugin.xml --- a/vm-gc/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/vm-gc/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 vm-heap-analysis/distribution/plugin.xml --- a/vm-heap-analysis/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/vm-heap-analysis/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> dump-heap diff -r 066ee2d7317c -r bf4f8d4e38a9 vm-memory/distribution/plugin.xml --- a/vm-memory/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/vm-memory/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui diff -r 066ee2d7317c -r bf4f8d4e38a9 vm-overview/distribution/plugin.xml --- a/vm-overview/distribution/plugin.xml Tue May 07 18:27:02 2013 -0400 +++ b/vm-overview/distribution/plugin.xml Mon May 13 12:39:57 2013 -0400 @@ -38,7 +38,7 @@ --> + xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 plugin.xsd"> gui