/* * Copyright (c) 2002-2012 Alibaba Group Holding Limited. * All rights reserved. * * Licensed 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 com.alibaba.citrus.springext.support; import static com.alibaba.citrus.util.Assert.*; import static com.alibaba.citrus.util.CollectionUtil.*; import static com.alibaba.citrus.util.StringUtil.*; import static java.lang.Math.*; import static javax.xml.XMLConstants.*; import static org.dom4j.DocumentHelper.*; import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.util.Formatter; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import com.alibaba.citrus.springext.ConfigurationPoint; import com.alibaba.citrus.springext.ConfigurationPoints; import com.alibaba.citrus.springext.Contribution; import com.alibaba.citrus.springext.ResourceResolver; import com.alibaba.citrus.springext.Schema; import com.alibaba.citrus.springext.Schema.Transformer; import com.alibaba.citrus.springext.impl.ConfigurationPointImpl; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.Namespace; import org.dom4j.Node; import org.dom4j.QName; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; /** * 处理schema和xml的工具。 * * @author Michael Zhou */ public class SchemaUtil { public static final String DEFAULT_LOCATION_PREFIX = "http://localhost:8080/schema/"; private static final String SPRINGEXT_BASE_URI = "http://www.alibaba.com/schema/springext/base"; private static final String SPRINGEXT_BASE_XSD = "http://www.alibaba.com/schema/springext/springext-base.xsd"; private final static Namespace XSD = DocumentHelper.createNamespace("xsd", W3C_XML_SCHEMA_NS_URI); private final static QName XSD_ANY = DocumentHelper.createQName("any", XSD); private final static QName XSD_IMPORT = DocumentHelper.createQName("import", XSD); private final static QName XSD_CHOICE = DocumentHelper.createQName("choice", XSD); private final static QName XSD_ELEMENT = DocumentHelper.createQName("element", XSD); private final static QName XSD_INCLUDE = DocumentHelper.createQName("include", XSD); /** 读取dom。 */ public static Document readDocument(InputStream istream, String systemId, boolean close) throws DocumentException { SAXReader reader = new SAXReader(false); Document doc = null; try { doc = reader.read(istream, systemId); } finally { if (close && istream != null) { try { istream.close(); } catch (Exception e) { // ignored } } } return doc; } /** 输出DOM。 */ public static String getDocumentText(Document doc, String charset) { StringWriter writer = new StringWriter(); try { writeDocument(doc, writer, charset); } catch (IOException e) { // 不会发生 unexpectedException(e); } return writer.toString(); } /** 输出DOM。 */ public static byte[] getDocumentContent(Document doc) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { writeDocument(doc, baos, null); } catch (IOException e) { unexpectedException(e); // 此调用不太可能报IO错 } return baos.toByteArray(); } /** 输出DOM。 */ public static void writeDocument(Document doc, OutputStream stream, String charset) throws IOException { charset = defaultIfEmpty(trimToNull(charset), "UTF-8"); writeDocument(doc, new OutputStreamWriter(stream, charset), charset); } /** 输出DOM。 */ public static void writeDocument(Document doc, Writer writer, String charset) throws IOException { charset = defaultIfEmpty(trimToNull(charset), "UTF-8"); OutputFormat format = OutputFormat.createPrettyPrint(); format.setEncoding(charset); format.setIndent(true); format.setIndentSize(4); XMLWriter xmlWriter = new XMLWriter(writer, format); xmlWriter.write(doc); xmlWriter.flush(); } /** 将所有contributions的schema汇总成一个schema。 */ public static Document createConfigurationPointSchema(ConfigurationPoint configurationPoint, String version) { Document doc = createDocument(); // <xsd:schema> Element schemaRoot = doc.addElement("xsd:schema", W3C_XML_SCHEMA_NS_URI); schemaRoot.addNamespace("xsd", W3C_XML_SCHEMA_NS_URI); schemaRoot.addNamespace("beans", BEANS_NAMESPACE_URI); schemaRoot.addNamespace("springext", SPRINGEXT_BASE_URI); schemaRoot.addNamespace("", configurationPoint.getNamespaceUri()); schemaRoot.addAttribute("targetNamespace", configurationPoint.getNamespaceUri()); // <xsd:include schemaLocation="contribution schema" /> Set<String> includings = createTreeSet(); for (Contribution contrib : configurationPoint.getContributions()) { Schema contribSchema = contrib.getSchemas().getVersionedSchema(version); if (contribSchema == null) { contribSchema = contrib.getSchemas().getMainSchema(); } if (contribSchema != null) { includings.add(contribSchema.getName()); } } for (String including : includings) { Element includeElement = schemaRoot.addElement("xsd:include"); includeElement.addAttribute("schemaLocation", including); } if (configurationPoint.getDefaultElementName() != null) { // <xsd:import namespace="http://www.springframework.org/schema/beans" // schemaLocation="http://www.springframework.org/schema/beans/spring-beans.xsd" /> Element importBeans = schemaRoot.addElement("xsd:import"); importBeans.addAttribute("namespace", BEANS_NAMESPACE_URI); importBeans.addAttribute("schemaLocation", BEANS_NAMESPACE_URI + "/spring-beans.xsd"); // <xsd:import namespace="http://www.alibaba.com/schema/springext/base" // schemaLocation="http://www.alibaba.com/schema/springext/springext-base.xsd" /> Element importSpringextBase = schemaRoot.addElement("xsd:import"); importSpringextBase.addAttribute("namespace", SPRINGEXT_BASE_URI); importSpringextBase.addAttribute("schemaLocation", SPRINGEXT_BASE_XSD); // <xsd:element name="defaultElementName" type="springext:referenceableBeanType" /> Element element = schemaRoot.addElement("xsd:element"); element.addAttribute("name", configurationPoint.getDefaultElementName()); element.addAttribute("type", "springext:referenceableBeanType"); } return doc; } /** * 取得contribution schema的内容,将其中引用其它configuration * point的element修改成具体的element名称。例如,将如下定义: * <p/> * <pre> * <xsd:any namespace="http://www.alibaba.com/schema/services/template/engines" /> * </pre> * <p> * 修改成: * </p> * <p/> * <pre> * <xsd:choose xmlns:tpl-engines="http://www.alibaba.com/schema/services/template/engines"> * <xsd:element ref="tpl-engines:velocity-engine" /> * <xsd:element ref="tpl-engines:freemarker-engine" /> * <xsd:element ref="tpl-engines:jsp-engine" /> * </xsd:choose> * </pre> * <p/> */ private static abstract class AnyElementTransformer implements Transformer { protected final ConfigurationPoints cps; protected Element root; private final Map<String, ConfigurationPoint> importings = createHashMap(); protected AnyElementTransformer(ConfigurationPoints cps) { this.cps = cps; } public final void transform(Document document, String systemId) { root = document.getRootElement(); if (!W3C_XML_SCHEMA_NS_URI.equals(root.getNamespaceURI()) || !"schema".equals(root.getName())) { return; } visitRootElement(); visitElement(root); // 避免加入重复的import @SuppressWarnings("unchecked") List<Element> importElements = root.elements(XSD_IMPORT); for (Element importElement : importElements) { if (importElement.attribute("namespace") != null) { importings.remove(importElement.attribute("namespace").getValue()); } } // 加入imports @SuppressWarnings("unchecked") List<Element> rootElements = root.elements(); int importIndex = 0; for (ConfigurationPoint cp : importings.values()) { if (canAddImportFor(cp)) { Element importElement = DocumentHelper.createElement(XSD_IMPORT); importElement.addAttribute("namespace", cp.getNamespaceUri()); importElement.addAttribute("schemaLocation", cp.getSchemas().getMainSchema().getName()); // XXX main or versioned? rootElements.add(importIndex++, importElement); } } } protected void visitRootElement() { } protected boolean canAddImportFor(ConfigurationPoint cp) { return true; } protected abstract void visitDependedConfigurationPoint(ConfigurationPoint cp); @SuppressWarnings("unchecked") private void visitElement(Element element) { List<Element> elements = element.elements(); visitElements(elements); } private void visitElements(List<Element> elements) { List<Integer> indexes = createLinkedList(); int index = 0; for (Element subElement : elements) { if (subElement.getQName().equals(XSD_ANY) && subElement.attribute("namespace") != null) { String ns = subElement.attribute("namespace").getValue(); ConfigurationPoint cp = cps.getConfigurationPointByNamespaceUri(ns); if (cp != null) { indexes.add(index); importings.put(ns, cp); } visitDependedConfigurationPoint(cp); } else { visitElement(subElement); } index++; } for (Integer i : indexes) { visitAnyElement(elements, i); } } private void visitAnyElement(List<Element> elements, int index) { Element anyElement = elements.get(index); String ns = anyElement.attribute("namespace").getValue(); ConfigurationPoint cp = cps.getConfigurationPointByNamespaceUri(ns); if (cp != null) { Element choiceElement = DocumentHelper.createElement(XSD_CHOICE); String nsPrefix = getNamespacePrefix(cp.getPreferredNsPrefix(), ns); // <xsd:schema xmlns:prefix="ns"> // 注意:必须将ns定义加在顶层element,否则低版本的xerces会报错。 root.addNamespace(nsPrefix, ns); // <xsd:choice minOccurs="?" maxOccurs="?" /> if (anyElement.attribute("minOccurs") != null) { choiceElement.addAttribute("minOccurs", anyElement.attribute("minOccurs").getValue()); } if (anyElement.attribute("maxOccurs") != null) { choiceElement.addAttribute("maxOccurs", anyElement.attribute("maxOccurs").getValue()); } // <xsd:element ref="prefix:contrib" /> for (Contribution contrib : cp.getContributions()) { Element elementElement = DocumentHelper.createElement(XSD_ELEMENT); elementElement.addAttribute("ref", nsPrefix + ":" + contrib.getName()); choiceElement.add(elementElement); } // <xsd:element ref="prefix:defaultName" /> if (cp.getDefaultElementName() != null) { Element elementElement = DocumentHelper.createElement(XSD_ELEMENT); elementElement.addAttribute("ref", nsPrefix + ":" + cp.getDefaultElementName()); choiceElement.add(elementElement); } // 用choice取代any elements.set(index, choiceElement); } } } public static Transformer getContributionSchemaTransformer(ConfigurationPoints cps, final Contribution thisContrib) { return new AnyElementTransformer(cps) { private final ConfigurationPoint thisCp = thisContrib.getConfigurationPoint(); @Override protected void visitRootElement() { // 为contribution schema添加默认namespace和targetNamespace的声明。 // 由于contribution schema是被include到configuration point schema中的,所以这些声明不是必须的。 // 但是Intellij IDEA似乎不能正确学习到contribution schema的namespace,除非加上这些声明。 root.addNamespace("", thisCp.getNamespaceUri()); root.addAttribute("targetNamespace", thisCp.getNamespaceUri()); } /** 避免import当前schema所在的configurtion point schema。 */ @Override protected boolean canAddImportFor(ConfigurationPoint cp) { return thisCp != cp; } @Override protected void visitDependedConfigurationPoint(ConfigurationPoint cp) { if (cp instanceof ConfigurationPointImpl) { ((ConfigurationPointImpl) cp).addDependingContribution(thisContrib); } } }; } public static interface AnyElementVisitor { void visitAnyElement(ConfigurationPoint cp); } public static Transformer getAnyElementTransformer(ConfigurationPoints cps, final AnyElementVisitor visitor) { return new AnyElementTransformer(cps) { @Override protected void visitDependedConfigurationPoint(ConfigurationPoint cp) { visitor.visitAnyElement(cp); } }; } /** 修改schema,除去所有的includes。 */ public static Transformer getTransformerWhoRemovesIncludes() { return new Transformer() { public void transform(Document document, String systemId) { Element root = document.getRootElement(); // <xsd:schema> if (W3C_XML_SCHEMA_NS_URI.equals(root.getNamespaceURI()) && "schema".equals(root.getName())) { // for each <xsd:include> for (Iterator<?> i = root.elementIterator(XSD_INCLUDE); i.hasNext(); ) { i.next(); i.remove(); } } } }; } /** 修改schema,添加间接依赖的includes。 */ public static Transformer getTransformerWhoAddsIndirectIncludes(final Map<String, Schema> includes) { return new Transformer() { public void transform(Document document, String systemId) { Element root = document.getRootElement(); root.addNamespace("xsd", W3C_XML_SCHEMA_NS_URI); // <xsd:schema> if (W3C_XML_SCHEMA_NS_URI.equals(root.getNamespaceURI()) && "schema".equals(root.getName())) { Namespace xsd = DocumentHelper.createNamespace("xsd", W3C_XML_SCHEMA_NS_URI); QName includeName = DocumentHelper.createQName("include", xsd); // for each <xsd:include> for (Iterator<?> i = root.elementIterator(includeName); i.hasNext(); ) { i.next(); i.remove(); } // 添加includes @SuppressWarnings("unchecked") List<Node> nodes = root.elements(); int i = 0; for (Schema includedSchema : includes.values()) { Element includeElement = DocumentHelper.createElement(includeName); nodes.add(i++, includeElement); includeElement.addAttribute("schemaLocation", includedSchema.getName()); } } } }; } /** 在所有可识别的URI上,加上指定前缀。 */ public static Transformer getAddPrefixTransformer(final SchemaSet schemas, String prefix) { if (prefix != null) { if (!prefix.endsWith("/")) { prefix += "/"; } } final String normalizedPrefix = prefix; return new Transformer() { public void transform(Document document, String systemId) { if (normalizedPrefix != null) { Element root = document.getRootElement(); // <xsd:schema> if (W3C_XML_SCHEMA_NS_URI.equals(root.getNamespaceURI()) && "schema".equals(root.getName())) { Namespace xsd = DocumentHelper.createNamespace("xsd", W3C_XML_SCHEMA_NS_URI); QName includeName = DocumentHelper.createQName("include", xsd); QName importName = DocumentHelper.createQName("import", xsd); // for each <xsd:include> for (Iterator<?> i = root.elementIterator(includeName); i.hasNext(); ) { Element includeElement = (Element) i.next(); String schemaLocation = trimToNull(includeElement.attributeValue("schemaLocation")); if (schemaLocation != null) { schemaLocation = getNewSchemaLocation(schemaLocation, null, systemId); if (schemaLocation != null) { includeElement.addAttribute("schemaLocation", schemaLocation); } } } // for each <xsd:import> for (Iterator<?> i = root.elementIterator(importName); i.hasNext(); ) { Element importElement = (Element) i.next(); String schemaLocation = importElement.attributeValue("schemaLocation"); String namespace = trimToNull(importElement.attributeValue("namespace")); if (schemaLocation != null || namespace != null) { schemaLocation = getNewSchemaLocation(schemaLocation, namespace, systemId); if (schemaLocation != null) { importElement.addAttribute("schemaLocation", schemaLocation); } } } } } } private String getNewSchemaLocation(String schemaLocation, String namespace, String systemId) { // 根据指定的schemaLocation判断 if (schemaLocation != null) { Schema schema = schemas.findSchema(schemaLocation); if (schema != null) { return normalizedPrefix + schema.getName(); } else { return schemaLocation; // 返回原本的location,但可能是错误的! } } // 再根据namespace判断 if (namespace != null) { Set<Schema> nsSchemas = schemas.getNamespaceMappings().get(namespace); if (nsSchemas != null && !nsSchemas.isEmpty()) { // 首先,在所有相同ns的schema中查找版本相同的schema。 String versionedExtension = getVersionedExtension(systemId); if (versionedExtension != null) { for (Schema schema : nsSchemas) { if (schema.getName().endsWith(versionedExtension)) { return normalizedPrefix + schema.getName(); } } } // 其次,选择第一个默认的schema,其顺序是:beans.xsd、beans-2.5.xsd、beans-2.0.xsd return normalizedPrefix + nsSchemas.iterator().next().getName(); } } return null; } /** 对于spring-aop-2.5.xsd取得-2.5.xsd。 */ private String getVersionedExtension(String systemId) { if (systemId != null) { int dashIndex = systemId.lastIndexOf("-"); int slashIndex = systemId.lastIndexOf("/"); if (dashIndex > slashIndex) { return systemId.substring(dashIndex); } } return null; } }; } /** 将内部element/attribute设置了不需要namespace。 */ public static Transformer getUnqualifiedStyleTransformer(ResourceResolver resourceResolver) { if (isUnqualifiedStyle(resourceResolver)) { return new Transformer() { public void transform(Document document, String systemId) { Element root = document.getRootElement(); if (root.attribute("elementFormDefault") != null) { root.remove(root.attribute("elementFormDefault")); } if (root.attribute("attributeFormDefault") != null) { root.remove(root.attribute("attributeFormDefault")); } } }; } else { return getNoopTransformer(); } } /** 判断是否为unqualified style。对于IDE plugins,这个判断可以让同一个plugin工作于不同的webx版本上。 */ private static boolean isUnqualifiedStyle(ResourceResolver resourceResolver) { // 支持unqualifed style的webx包含converter类,早期的版本则没有这个类。 // 因此可用这个类来区分webx的版本。 return resourceResolver.getResource("com/alibaba/citrus/springext/util/ConvertToUnqualifiedStyle.class") != null; } public static Transformer getNoopTransformer() { return new Transformer() { public void transform(Document document, String systemId) { // donothing } }; } public static String getNamespacePrefix(String preferredNsPrefix, String targetNamespace) { if (preferredNsPrefix != null) { return preferredNsPrefix; } if (targetNamespace != null) { return targetNamespace.substring(targetNamespace.lastIndexOf("/") + 1); } return null; } public static Map<String, String> parseSchemaLocation(String value) { Map<String, String> locations = createTreeMap(); value = trimToNull(value); if (value != null) { String[] values = value.split("\\s+"); for (int i = 0; i < values.length - 1; i += 2) { String uri = trimToNull(values[i]); String location = trimToNull(values[i + 1]); if (uri != null && location != null) { locations.put(uri, location); } } } return locations; } /** * 格式化schemaLocation,使之符合以下易读的格式: * <pre> * <beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" * ... * xsi:schemaLocation=" * http://www.alibaba.com/schema/services http://localhost:8080/schema/services.xsd * http://www.alibaba.com/schema/services/resource-loading/loaders http://localhost:8080/schema/services-resource-loading-loaders.xsd * http://www.springframework.org/schema/beans http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd * "> * </pre> */ public static String formatSchemaLocations(Map<String, String> schemaLocations, String qualifiedRootElementName) { StringBuilder buf = new StringBuilder(); Formatter formatter = new Formatter(buf); try { String leadingSpaces = qualifiedRootElementName.replaceAll(".", " ") + " "; String indent = " "; formatter.format("%n"); int maxLength = 0; for (String ns : schemaLocations.keySet()) { maxLength = max(maxLength, ns.length()); } String format = leadingSpaces + indent + "%-" + maxLength + "s %s%n"; for (Map.Entry<String, String> entry : schemaLocations.entrySet()) { String ns = entry.getKey(); String location = entry.getValue(); formatter.format(format, ns, location); } formatter.format(leadingSpaces); } finally { formatter.close(); } return buf.toString(); } public static String guessLocationPrefix(Map<String, String> schemaLocations, SchemaSet schemas) { for (Map.Entry<String, String> entry : schemaLocations.entrySet()) { String uri = entry.getKey(); String location = entry.getValue(); Set<Schema> schemaSet = schemas.getNamespaceMappings().get(uri); if (schemaSet != null) { for (Schema schema : schemaSet) { if (location.endsWith(schema.getName())) { String prefix = location.substring(0, location.length() - schema.getName().length()); try { new URI(prefix); // 避免出现"http://" return prefix; } catch (URISyntaxException ignored) { } } } } } return DEFAULT_LOCATION_PREFIX; } }