/*
* Copyright (c) 2002-2013 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.util;
import static com.alibaba.citrus.springext.support.SchemaUtil.*;
import static com.alibaba.citrus.util.ArrayUtil.*;
import static com.alibaba.citrus.util.Assert.*;
import static com.alibaba.citrus.util.CollectionUtil.*;
import static com.alibaba.citrus.util.StringUtil.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import com.alibaba.citrus.logconfig.LogConfigurator;
import com.alibaba.citrus.springext.ConfigurationPoint;
import com.alibaba.citrus.springext.ContributionType;
import com.alibaba.citrus.springext.Schema;
import com.alibaba.citrus.springext.support.SchemaUtil;
import com.alibaba.citrus.springext.support.SpringExtSchemaSet;
import com.alibaba.citrus.util.FileUtil;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 将一个webx配置文件转换成unqualifed风格。
*
* @author Michael Zhou
*/
public class ConvertToUnqualifiedStyle {
private final Logger log = LoggerFactory.getLogger(getClass());
private final File[] sources;
private final SpringExtSchemaSet schemas;
private final boolean forceConvert;
private final boolean backup;
private int convertedCount;
/** 必须运行在适当的classpath下,否则不能取得configuration points。 */
public static void main(String[] args) {
File[] sources = new File[args.length];
for (int i = 0; i < args.length; i++) {
sources[i] = new File(args[i]).getAbsoluteFile();
}
new ConvertToUnqualifiedStyle(sources).convert();
}
public ConvertToUnqualifiedStyle(File[] sources) {
this(sources, false, true);
}
public ConvertToUnqualifiedStyle(File[] sources, boolean forceConvert, boolean backup) {
LogConfigurator.getConfigurator().configureDefault();
this.sources = sources;
this.schemas = new SpringExtSchemaSet();
this.forceConvert = forceConvert;
this.backup = backup;
}
public void convert() {
if (!isEmptyArray(sources)) {
for (File source : sources) {
convert(source);
}
}
log.info("Converted {} files.", convertedCount);
}
private final Pattern backupFilePattern = Pattern.compile("\\.bak$");
private void convert(File source) {
if (backupFilePattern.matcher(source.getName()).find()) {
return; // skip backup file
}
Document doc;
try {
doc = SchemaUtil.readDocument(new FileInputStream(source), source.getAbsolutePath(), true);
} catch (Exception e) {
log.warn("Failed to read file {}: {}", getRelativePath(source), e.getMessage());
return;
}
if (!isSpringConfigurationFile(doc)) {
return;
}
log.info("Converting file: {}", getRelativePath(source));
boolean modified = new Converter(doc, schemas).convert();
if (modified || forceConvert) {
File dir = source.getParentFile();
String fileName = source.getName();
int index = fileName.lastIndexOf(".");
String ext = "";
if (index >= 0) {
ext = fileName.substring(index);
fileName = fileName.substring(0, index);
}
File tmpFile = null;
FileOutputStream fos = null;
boolean failed = false;
try {
tmpFile = File.createTempFile(fileName + "_tmp_", ext, dir);
fos = new FileOutputStream(tmpFile);
writeDocument(doc, fos);
} catch (IOException e) {
log.warn("Failed to write to {}: {}", getRelativePath(tmpFile), e.getMessage());
failed = true;
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException ignored) {
}
}
}
if (failed) {
tmpFile.delete();
} else {
File backupFile = null;
if (backup) {
for (int i = 0; ; i++) {
backupFile = new File(dir, fileName + (i == 0 ? "" : "_" + i) + ext + ".bak");
if (!backupFile.exists()) {
break;
}
}
source.renameTo(backupFile);
log.info(" ... converted, original content saved as {}", getRelativePath(backupFile));
} else {
source.delete();
log.info(" ... converted");
}
tmpFile.renameTo(source);
convertedCount++;
}
} else {
log.info(" ... skipped");
}
}
private static void writeDocument(Document doc, OutputStream stream) throws IOException {
String charset = "UTF-8";
Writer writer = new OutputStreamWriter(stream, charset);
OutputFormat format = new OutputFormat();
format.setEncoding(charset);
XMLWriter xmlWriter = new XMLWriter(writer, format);
xmlWriter.write(doc);
xmlWriter.flush();
}
/** Root element是否为<code><beans:bean></code>? */
private boolean isSpringConfigurationFile(Document doc) {
Element root = doc.getRootElement();
return "http://www.springframework.org/schema/beans".equals(root.getNamespaceURI())
&& "beans".equals(root.getName());
}
private String getRelativePath(File f) {
return FileUtil.getRelativePath(new File("").getAbsolutePath(), f.getAbsolutePath());
}
public static class Converter {
private final SpringExtSchemaSet schemas;
private final Element root;
private final LinkedList<String> namespaceURIStack = createLinkedList();
private final Map<String, Namespace> configurationPointNamespaces = createTreeMap();
private final Map<String, Namespace> allSchemaNamespaces = createTreeMap();
private boolean modified = false;
public Converter(Document doc, SpringExtSchemaSet schemas) {
this.root = doc.getRootElement();
this.schemas = schemas;
}
public boolean convert() {
visit(root);
return modified;
}
private void visit(Element element) {
String namespaceURI = trimToNull(element.getNamespaceURI());
List<Namespace> nsToBeRemoved = createLinkedList();
for (Iterator<?> i = element.declaredNamespaces().iterator(); i.hasNext(); ) {
Namespace ns = (Namespace) i.next();
if (isConfigurationPointNamespace(ns.getURI())) {
nsToBeRemoved.add(ns);
if (element != root) {
modified = true;
}
if (!configurationPointNamespaces.containsKey(ns.getURI())) {
if (isEmpty(ns.getPrefix())) {
String prefix = getNamespacePrefix(
schemas.getConfigurationPoints().getConfigurationPointByNamespaceUri(ns.getURI()).getPreferredNsPrefix(), ns.getURI());
ns = Namespace.get(prefix, ns.getURI());
modified = true;
}
configurationPointNamespaces.put(ns.getURI(), ns);
allSchemaNamespaces.put(ns.getURI(), ns);
}
} else if (schemas.getNamespaceMappings().containsKey(ns.getURI())) {
if (!allSchemaNamespaces.containsKey(ns.getURI())) {
allSchemaNamespaces.put(ns.getURI(), ns);
}
}
}
// 如果当前没有指定ns,或不是configuration point ns
if (!isConfigurationPointNamespace(namespaceURI)) {
visitSubElements(element);
}
// 从这里开始,ns不可能是null,并且代表一个configuration point ns。
// 如果当前ns和context ns不同
else if (!namespaceURI.equals(getContextNamespaceURI())
|| isContributionElement(namespaceURI, element.getName())) {
// 此namespaceURI应该已经在某处被定义了,所以namespaces.get(namespaceURI)不应为空。
Namespace ns = assertNotNull(configurationPointNamespaces.get(namespaceURI), "xmlns not defined for %s", namespaceURI);
try {
namespaceURIStack.push(namespaceURI);
setNamespacePrefix(element, ns.getPrefix());
visitSubElements(element);
} finally {
namespaceURIStack.pop();
}
}
// 如果当前ns和context ns相同
else {
removeNamespace(element);
visitSubElements(element);
modified = true;
}
// 先删除所有的ns声明、attributes,一会儿排序以后再加回去。
for (Namespace ns : nsToBeRemoved) {
element.remove(ns);
}
// 将所有ns加回到root element。
if (element == root) {
Map<String, Namespace> otherNamespaces = createTreeMap();
Map<String, Attribute> otherAttrs = createTreeMap();
Namespace xsi = null;
String schemaLocation = null;
for (Iterator<?> i = element.declaredNamespaces().iterator(); i.hasNext(); ) {
Namespace ns = (Namespace) i.next();
if ("http://www.w3.org/2001/XMLSchema-instance".equals(ns.getURI())) {
xsi = ns;
} else if (!allSchemaNamespaces.containsKey(ns.getURI())) {
otherNamespaces.put(ns.getURI(), ns);
}
i.remove();
}
QName schemaLocationQName = QName.get("schemaLocation", xsi);
for (Iterator<?> i = element.attributes().iterator(); i.hasNext(); ) {
Attribute attr = (Attribute) i.next();
if (schemaLocationQName.equals(attr.getQName())) {
schemaLocation = attr.getText();
} else {
otherAttrs.put(attr.getQualifiedName(), attr);
}
i.remove();
}
// xmlns:xsi
if (xsi == null) {
xsi = DocumentHelper.createNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
}
element.add(xsi);
// namespaces in defined schemas
for (Namespace ns : allSchemaNamespaces.values()) {
element.add(ns);
}
// other xmlns attrs
for (Namespace ns : otherNamespaces.values()) {
element.add(ns);
}
// schema location
element.addAttribute(schemaLocationQName, reformatSchemaLocations(schemaLocation));
// other attrs
for (Attribute attr : otherAttrs.values()) {
element.add(attr);
}
}
}
private boolean isContributionElement(String uri, String name) {
ConfigurationPoint cp = schemas.getConfigurationPoints().getConfigurationPointByNamespaceUri(uri);
if (cp != null) {
return name.equals(cp.getDefaultElementName()) // default element
|| cp.getContribution(name, ContributionType.BEAN_DEFINITION_PARSER) != null // contribution
|| cp.getContribution(name, ContributionType.BEAN_DEFINITION_DECORATOR) != null; // contribution
}
return false;
}
private boolean isConfigurationPointNamespace(String namespaceURI) {
return schemas.getConfigurationPoints().getConfigurationPointByNamespaceUri(namespaceURI) != null;
}
private String reformatSchemaLocations(String schemaLocation) {
Map<String, String> schemaLocations = parseSchemaLocation(schemaLocation);
String locationPrefix = guessLocationPrefix(schemaLocations, schemas);
// 将缺少的schema location补充完整。
for (String namespaceURI : allSchemaNamespaces.keySet()) {
if (!schemaLocations.containsKey(namespaceURI)) {
try {
Set<Schema> schemaSet = schemas.getNamespaceMappings().get(namespaceURI);
if (schemaSet != null && schemaSet.size() > 0) {
schemaLocations.put(namespaceURI, locationPrefix + schemaSet.iterator().next().getName());
modified = true;
}
} catch (Exception ignored) {
}
}
}
return formatSchemaLocations(schemaLocations, root.getQualifiedName());
}
/** 将element的prefix改成统一的值,但不改变其namespace。 */
private void setNamespacePrefix(Element element, String prefix) {
assertNotNull(prefix, "prefix is null");
if (!prefix.equals(element.getNamespacePrefix())) {
element.setQName(QName.get(element.getName(), prefix, element.getNamespaceURI()));
modified = true;
}
}
/** 将element变成unqualified。 */
private void removeNamespace(Element element) {
if (!isEmpty(element.getNamespaceURI())) {
element.setQName(QName.get(element.getName()));
modified = true;
}
}
private void visitSubElements(Element element) {
for (Object subElement : element.elements()) {
visit((Element) subElement);
}
}
private String getContextNamespaceURI() {
if (namespaceURIStack.isEmpty()) {
return null;
}
return namespaceURIStack.peek();
}
}
}