/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.karaf.features.internal.model; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; import java.net.URL; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.xml.XMLConstants; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import org.apache.karaf.features.FeaturesNamespaces; import org.apache.karaf.util.XmlUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLFilter; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLFilterImpl; public final class JaxbUtil { private static final Logger LOGGER = LoggerFactory.getLogger(JaxbUtil.class); private static final JAXBContext FEATURES_CONTEXT; private static final Map<String, Schema> SCHEMAS = new ConcurrentHashMap<>(); static { try { FEATURES_CONTEXT = JAXBContext.newInstance(Features.class); } catch (JAXBException e) { throw new RuntimeException(e); } } private JaxbUtil() { } public static void marshal(Features features, OutputStream out) throws JAXBException { Marshaller marshaller = FEATURES_CONTEXT.createMarshaller(); marshaller.setProperty("jaxb.formatted.output", true); marshaller.marshal(features, out); } public static void marshal(Features features, Writer out) throws JAXBException { Marshaller marshaller = FEATURES_CONTEXT.createMarshaller(); marshaller.setProperty("jaxb.formatted.output", true); marshaller.marshal(features, out); } /** * Read in a Features from the input stream. * * @param uri uri to read * @param validate whether to validate the input. * @return a Features read from the input stream */ public static Features unmarshal(String uri, boolean validate) { return unmarshal(uri, null, validate); } public static Features unmarshal(String uri, InputStream stream, boolean validate) { Features features; if (validate) { features = unmarshalValidate(uri, stream); } else { features = unmarshalNoValidate(uri, stream); } features.postUnmarshall(uri); return features; } private static Features unmarshalValidate(String uri, InputStream stream) { try { Document doc; if (stream != null) { doc = XmlUtils.parse(stream); doc.setDocumentURI(uri); } else { doc = XmlUtils.parse(uri); } String nsuri = doc.getDocumentElement().getNamespaceURI(); if (nsuri == null) { LOGGER.warn("Old style feature file without namespace found (URI: {}). This format is deprecated and support for it will soon be removed", uri); } else { Schema schema = getSchema(nsuri); try { schema.newValidator().validate(new DOMSource(doc)); } catch (SAXException e) { throw new IllegalArgumentException("Unable to validate " + uri, e); } } fixDom(doc, doc.getDocumentElement()); Unmarshaller unmarshaller = FEATURES_CONTEXT.createUnmarshaller(); Features features = (Features) unmarshaller.unmarshal(new DOMSource(doc)); features.setNamespace(nsuri); return features; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException("Unable to load " + uri, e); } } private static Schema getSchema(String namespace) throws SAXException { Schema schema = SCHEMAS.get(namespace); if (schema == null) { String schemaLocation; switch (namespace) { case FeaturesNamespaces.URI_1_0_0: schemaLocation = "/org/apache/karaf/features/karaf-features-1.0.0.xsd"; break; case FeaturesNamespaces.URI_1_1_0: schemaLocation = "/org/apache/karaf/features/karaf-features-1.1.0.xsd"; break; case FeaturesNamespaces.URI_1_2_0: schemaLocation = "/org/apache/karaf/features/karaf-features-1.2.0.xsd"; break; case FeaturesNamespaces.URI_1_2_1: schemaLocation = "/org/apache/karaf/features/karaf-features-1.2.1.xsd"; break; case FeaturesNamespaces.URI_1_3_0: schemaLocation = "/org/apache/karaf/features/karaf-features-1.3.0.xsd"; break; case FeaturesNamespaces.URI_1_4_0: schemaLocation = "/org/apache/karaf/features/karaf-features-1.4.0.xsd"; break; case FeaturesNamespaces.URI_1_5_0: schemaLocation = "/org/apache/karaf/features/karaf-features-1.5.0.xsd"; break; default: throw new IllegalArgumentException("Unsupported namespace: " + namespace); } SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); // root element has namespace - we can use schema validation URL url = JaxbUtil.class.getResource(schemaLocation); if (url == null) { throw new IllegalStateException("Could not find resource: " + schemaLocation); } schema = factory.newSchema(new StreamSource(url.toExternalForm())); SCHEMAS.put(namespace, schema); } return schema; } private static void fixDom(Document doc, Node node) { if (node.getNodeType() == Node.ELEMENT_NODE) { if (!FeaturesNamespaces.URI_CURRENT.equals(node.getNamespaceURI())) { doc.renameNode(node, FeaturesNamespaces.URI_CURRENT, node.getLocalName()); } NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { fixDom(doc, children.item(i)); } } } private static Features unmarshalNoValidate(String uri, InputStream stream) { try { Unmarshaller unmarshaller = FEATURES_CONTEXT.createUnmarshaller(); NoSourceAndNamespaceFilter xmlFilter = new NoSourceAndNamespaceFilter(XmlUtils.xmlReader()); xmlFilter.setContentHandler(unmarshaller.getUnmarshallerHandler()); InputSource is = new InputSource(uri); if (stream != null) { is.setByteStream(stream); } SAXSource source = new SAXSource(xmlFilter, is); Features features = (Features) unmarshaller.unmarshal(source); features.setNamespace(xmlFilter.getNamespace()); return features; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException("Unable to load " + uri, e); } } /** * Provides an empty inputsource for the entity resolver. * Converts all elements to the features namespace to make old feature files * compatible to the new format */ public static class NoSourceAndNamespaceFilter extends XMLFilterImpl { private static final InputSource EMPTY_INPUT_SOURCE = new InputSource(new ByteArrayInputStream(new byte[0])); private String namespace; public NoSourceAndNamespaceFilter(XMLReader xmlReader) { super(xmlReader); } @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { return EMPTY_INPUT_SOURCE; } @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { if ("features".equals(localName)) { namespace = uri; } super.startElement(FeaturesNamespaces.URI_CURRENT, localName, qName, atts); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { super.endElement(FeaturesNamespaces.URI_CURRENT, localName, qName); } public String getNamespace() { return namespace; } } }