package org.dcache.boot; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.w3c.dom.Document; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.util.Properties; import org.dcache.util.ConfigurationProperties; import static org.junit.Assert.*; /** * A series of tests to verify that the XmlEntityLayoutPrinter provides * XML entity definitions that work as expected. */ public class XmlEntityLayoutPrinterTests { private static final String XML_ENCODING = "UTF-8"; private static final String XPATH_EXPRESSION_FOR_TEST_ELEMENT = "/" + ParserContext.XML_TEST_ELEMENT_NAME; private static final String DEFAULT_PROPERTY_KEY = "default"; private static final String DEFAULT_PROPERTY_VALUE = "default value"; ConfigurationProperties globalProperties; Layout layout; @Before public void setUp() { ConfigurationProperties defaults = new ConfigurationProperties( new Properties()); defaults.setProperty(DEFAULT_PROPERTY_KEY, DEFAULT_PROPERTY_VALUE); layout = new Layout(defaults); globalProperties = layout.properties(); } @Test public void testDefaultProperty() throws IOException { assertEntityHasValue(DEFAULT_PROPERTY_KEY, DEFAULT_PROPERTY_VALUE); } @Test public void testOverwriteDefaultProperty() throws IOException { String newValue = "a new value"; globalProperties.setProperty(DEFAULT_PROPERTY_KEY, newValue); assertEntityHasValue(DEFAULT_PROPERTY_KEY, newValue); } @Test public void testPropertyValueWithQuotes() throws IOException { String key = "key"; String value = "He said \"what?\" before leaving."; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); } @Test public void testPropertyValueWithAmpersand() throws IOException { String key = "key"; String value = "this&that"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); } @Test public void testPropertyValueWithApostrophe() throws IOException { String key = "key"; String value = "She said 'what?' before leaving."; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); } @Test public void testPropertyValueWithLessThan() throws IOException { String key = "key"; String value = "1 < 2"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); } @Test public void testPropertyValueWithGreaterThan() throws IOException { String key = "key"; String value = "2 > 1"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); } @Test @Ignore public void testPropertyNameWithDot() throws IOException { String key = "key.value"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); assertEntityNotDefined("key_value"); } @Test @Ignore public void testPropertyNameWithDash() throws IOException { String key = "key-value"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); assertEntityNotDefined("key_value"); } @Test @Ignore public void testPropertyNameWithDigits() throws IOException { String key = "key25"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); assertEntityNotDefined("key__"); } @Test public void testPropertyNameWithUnderscore() throws IOException { String key = "interesting_key"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); } @Test public void testPropertyNameStartsWithUnderscore() throws IOException { String key = "_key"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(key, value); } @Test public void testIllegalPropertyNameStartsWithDot() throws IOException { String key = ".key"; String mappedKey = "_key"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(mappedKey, value); } @Test public void testIllegalPropertyNameStartsWithDigit() throws IOException { String key = "25key"; String mappedKey = "_5key"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(mappedKey, value); } @Test public void testIllegalPropertyNameStartsWithDash() throws IOException { String key = "-key"; String mappedKey = "_key"; String value = "some information"; globalProperties.setProperty(key, value); assertEntityHasValue(mappedKey, value); } @Test public void testExpandingReference() throws IOException { String key = "key"; String value = "${" + DEFAULT_PROPERTY_KEY + "}"; globalProperties.setProperty(key, value); assertEntityHasValue(key, DEFAULT_PROPERTY_VALUE); } @Test @Ignore public void testScopedPropertiesIgnored() throws IOException { String key = "scope/key"; String value = "some simple value"; globalProperties.setProperty(key, value); String mappedKey = "scope_key"; assertEntityNotDefined(mappedKey); } /* * Support methods and classes */ private void assertEntityHasValue(String entityName, String expectedValue) { ParserContext context = new ParserContext(entityName); XPathExpression expression = buildExpression(); String observedValue; try { Document doc = context.parse(); observedValue = expression.evaluate(doc); } catch (XPathExpressionException e) { throw new RuntimeException(e); } catch (SAXException e) { fail(e.getMessage()); return; } assertFalse(context.hasEntityResolvingError()); assertEquals(expectedValue, observedValue); } private void assertEntityNotDefined(String entityName) { ParserContext context = new ParserContext(entityName); XPathExpression expression = buildExpression(); try { Document doc = context.parse(); expression.evaluate(doc); } catch (SAXException e) { assertTrue( context.hasEntityResolvingError()); return; } catch (XPathExpressionException e) { throw new RuntimeException(e); } fail("Entity " + entityName + " was expanded successfully."); } private XPathExpression buildExpression() { XPath xpath = XPathFactory.newInstance().newXPath(); try { return xpath.compile(XPATH_EXPRESSION_FOR_TEST_ELEMENT); } catch (XPathExpressionException e) { throw new RuntimeException(e); } } /** * A context for building and parsing a predefined XML data. The * predefined XML tests contains a single XML element that encompasses * a single entity. The document also includes the file with public ID of * <tt>-//dCache//ENTITIES dCache Properties//EN</tt>. This allows the inclusion * of dCache property values as entities using a custom EntityResolver. * A custom ErrorHandler is used to catch potential problems expanding an * entity reference. * <p> * The overall effect is that calling {@link #parse} will build a Document * object that represents the value of a dCache property, if that property * exist; if the entity isn't defined then a SAXException is thrown. The * {@link #hasEntityResolvingError} method returns true if the test * entity could not be resolved or false otherwise. */ private class ParserContext { private static final String XML_TEST_ELEMENT_NAME = "test"; private final DocumentBuilder _db; private final InputSource _source; private final RecordingErrorHandler _errorHandler; ParserContext(String entityName) { String data = buildTestXml(entityName); _source = new InputSource( new StringReader(data)); try { _db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new RuntimeException(e); } EntityResolver resolver = new DcachePropertiesEntityResolver(); _db.setEntityResolver(resolver); _errorHandler = new RecordingErrorHandler(entityName); _db.setErrorHandler(_errorHandler); } /** * Build Document corresponding to: * <p> * <code> * <test>&entityName;</test> * </code> * with access to dCache properties as XML entities. */ public Document parse() throws SAXException { try { return _db.parse(_source); } catch (IOException e) { throw new RuntimeException(e); } } public boolean hasEntityResolvingError() { return _errorHandler.hasEntityResolvingError(); } private String buildTestXml(String entityName) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); PrintStream out = new PrintStream(stream); out.println("<?xml version=\"1.0\" encoding=\"" + XML_ENCODING + "\"?>"); out.println("<!DOCTYPE " + XML_TEST_ELEMENT_NAME + " ["); out.println("<!ENTITY % properties-data PUBLIC \"" + DcachePropertiesEntityResolver.PUBLIC_NAME + "\" \"/\">"); out.println("%properties-data;"); out.println("]>"); out.println("<" + XML_TEST_ELEMENT_NAME + ">&" + entityName + ";</" + XML_TEST_ELEMENT_NAME + ">"); out.flush(); try { return stream.toString(XML_ENCODING); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } } /** * SAX ErrorHandler that remembers whether a document is invalid due to * an entity that cannot be resolved. No output is emitted to stdout * or stderr. */ private class RecordingErrorHandler implements ErrorHandler { private boolean _entityResolvingError; private final String _entityResolvingMessage; RecordingErrorHandler(String entityName) { _entityResolvingMessage = "The entity \"" + entityName + "\" was referenced, but not declared."; } public boolean hasEntityResolvingError() { return _entityResolvingError; } @Override public void error(SAXParseException exception) throws SAXException { } @Override public void fatalError(SAXParseException exception) throws SAXException { // This is ugly, but seems the only way to detect an entity resolving error. if( exception.getMessage().equals(_entityResolvingMessage)) { _entityResolvingError = true; } } @Override public void warning(SAXParseException exception) throws SAXException { } } /** * This class provides an alternative {@link EntityResolver} that may be * used for any XML SAX-based operation. * <p> * If the requested entity has a public ID of: * <p> * <tt>-//dCache//ENTITIES dCache Properties//EN</tt> * <p> * then the system ID is ignored and a list of auto-generated entities, * based on the {@link XmlEntityLayoutPrinter} class, is supplied. All * other requests are resolved by the default handler, which will load * the corresponding files normally. */ private class DcachePropertiesEntityResolver implements EntityResolver { public static final String PUBLIC_NAME = "-//dCache//ENTITIES dCache Properties//EN"; private final EntityResolver _inner = new DefaultHandler(); @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { InputSource result; if( PUBLIC_NAME.equals(publicId)) { String xml = buildPropertiesFile(); result = new InputSource(new StringReader(xml)); } else { result = _inner.resolveEntity(publicId, systemId); } return result; } private String buildPropertiesFile() throws IOException { StringBuilder sb = new StringBuilder(); sb.append("<?xml version=\"1.0\" encoding=\"" + XML_ENCODING + "\"?>\n"); ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(out); LayoutPrinter printer = new XmlEntityLayoutPrinter(layout); printer.print(ps); ps.flush(); sb.append(out.toString(XML_ENCODING)); return sb.toString(); } } }