package org.jvnet.jaxb2_commons.plugin.namespace_prefix;
import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlSchema;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JAnnotationValue;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JStringLiteral;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.generator.bean.PackageOutlineImpl;
import com.sun.tools.xjc.model.CCustomizations;
import com.sun.tools.xjc.model.CPluginCustomization;
import com.sun.tools.xjc.model.Model;
import com.sun.tools.xjc.outline.Outline;
import com.sun.tools.xjc.outline.PackageOutline;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BIDeclaration;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BIXPluginCustomization;
import com.sun.tools.xjc.reader.xmlschema.bindinfo.BindInfo;
import com.sun.xml.xsom.XSAnnotation;
import com.sun.xml.xsom.XSSchema;
import com.sun.xml.xsom.impl.SchemaImpl;
import org.xml.sax.ErrorHandler;
/**
* This plugin adds {@link javax.xml.bind.annotation.XmlNs} annotations to <i>package-info.java</i> files. Those annotations tells Jaxb2 to generate XML schema's instances with specific namespaces
* prefixes, instead of the auto-generated (ns1, ns2, ...) prefixes. Definition of thoses prefixes is done in the bindings.xml file.
* <p/>
* Bindings.xml file example:
* <pre>
* <?xml version="1.0"?>
* <jxb:bindings version="2.1"
* xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
* xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
* xmlns:namespace="http://jaxb2-commons.dev.java.net/namespace-prefix">
*
* <jxb:bindings schemaLocation="unireg-common-1.xsd">
* <jxb:schemaBindings>
* <jxb:package name="ch.vd.unireg.xml.common.v1" />
* </jxb:schemaBindings>
* <jxb:bindings>
* <b><namespace:prefix name="common-1" /></b>
* </jxb:bindings>
* </jxb:bindings>
*
* </jxb:bindings>
* </pre>
*
* @author Manuel Siggen (c) 2012 Etat-de-Vaud (www.vd.ch)
*/
@SuppressWarnings("UnusedDeclaration")
public class NamespacePrefixPlugin extends Plugin {
private static final String NAMESPACE_URI = "http://jaxb2-commons.dev.java.net/namespace-prefix";
@Override
public String getOptionName() {
return "Xnamespace-prefix";
}
@Override
public String getUsage() {
return "-Xnamespace-prefix : activate namespaces prefix customizations";
}
@Override
public List<String> getCustomizationURIs() {
return Arrays.asList(NAMESPACE_URI);
}
@Override
public boolean isCustomizationTagName(String nsUri, String localName) {
return NAMESPACE_URI.equals(nsUri) && "prefix".equals(localName);
}
@Override
public boolean run(final Outline outline, final Options options, final ErrorHandler errorHandler) {
final JClass xmlNsClass = outline.getCodeModel().ref(XmlNs.class);
final JClass xmlSchemaClass = outline.getCodeModel().ref(XmlSchema.class);
for (PackageOutline packageOutline : outline.getAllPackageContexts()) {
final JPackage p = packageOutline._package();
// get the target namespaces of all schemas that bind to the current package
final Set<String> packageNamespaces = getPackageNamespace(packageOutline);
// is there any prefix binding defined for the current package ?
final Model packageModel = getPackageModel((PackageOutlineImpl) packageOutline);
final List<Pair> list = getPrefixBinding(packageModel, packageNamespaces);
acknowledgePrefixAnnotations(packageModel);
if (list == null || list.isEmpty()) {
// no prefix binding, nothing to do
continue;
}
// add XML namespace prefix annotations
final JAnnotationUse xmlSchemaAnnotation = getOrAddXmlSchemaAnnotation(p, xmlSchemaClass);
if (xmlSchemaAnnotation == null) {
throw new RuntimeException("Unable to get/add 'XmlSchema' annotation to package [" + p.name() + "]");
}
final JAnnotationArrayMember members = xmlSchemaAnnotation.paramArray("xmlns");
for (Pair pair : list) {
addNamespacePrefix(xmlNsClass, members, pair.getNamespace(), pair.getPrefix());
}
}
return true;
}
private static Set<String> getPackageNamespace(PackageOutline packageOutline) {
final Map<String, Integer> map = getUriCountMap(packageOutline);
return map == null ? Collections.<String>emptySet() : map.keySet();
}
/**
* Make sure the prefix annotations have been acknowledged.
*
* @param packageModel the package model
*/
private void acknowledgePrefixAnnotations(Model packageModel) {
final CCustomizations customizations = packageModel.getCustomizations();
if (customizations != null) {
for (CPluginCustomization customization : customizations) {
if (customization.element.getNamespaceURI().equals(NAMESPACE_URI)) {
if (!customization.element.getLocalName().equals("prefix")) {
throw new RuntimeException("Unrecognized element [" + customization.element.getLocalName() + "]");
}
customization.markAsAcknowledged();
}
}
}
}
/**
* This method detects prefixes for a given package as specified in the bindings file. Usually, there is only one namespace per package, but there may be more.
*
* @param packageModel the package model
* @param packageNamespace the target namespace for the package
* @return the prefix annotations
*/
private static List<Pair> getPrefixBinding(Model packageModel, Set<String> packageNamespace) {
final List<Pair> list = new ArrayList<Pair>();
// loop on existing schemas (XSD files)
for (XSSchema schema : packageModel.schemaComponent.getSchemas()) {
final SchemaImpl s = (SchemaImpl) schema;
final XSAnnotation annotation = s.getAnnotation();
if (annotation == null) {
continue;
}
final Object anno = annotation.getAnnotation();
if (anno == null || !(anno instanceof BindInfo)) {
continue;
}
final BindInfo b = (BindInfo) anno;
final String targetNS = b.getOwner().getOwnerSchema().getTargetNamespace();
if (!packageNamespace.contains(targetNS)) { // only consider schemas that bind the current package
continue;
}
// get the prefix's name
String prefix = "";
for (BIDeclaration declaration : b.getDecls()) {
if (declaration instanceof BIXPluginCustomization) {
final BIXPluginCustomization customization = (BIXPluginCustomization) declaration;
if (customization.element.getNamespaceURI().equals(NAMESPACE_URI)) {
if (!customization.element.getLocalName().equals("prefix")) {
throw new RuntimeException("Unrecognized element [" + customization.element.getLocalName() + "]");
}
prefix = customization.element.getAttribute("name");
customization.markAsAcknowledged();
break;
}
}
}
list.add(new Pair(targetNS, prefix));
}
return list;
}
private static void addNamespacePrefix(JClass xmlNsClass, JAnnotationArrayMember members, String namespace, String prefix) {
final JAnnotationUse ns = members.annotate(xmlNsClass);
ns.param("namespaceURI", namespace);
ns.param("prefix", prefix);
}
@SuppressWarnings("unchecked")
private static JAnnotationUse getOrAddXmlSchemaAnnotation(JPackage p, JClass xmlSchemaClass) {
JAnnotationUse xmlAnn = null;
final List<JAnnotationUse> annotations = getAnnotations(p);
if (annotations != null) {
for (JAnnotationUse annotation : annotations) {
final JClass clazz = getAnnotationJClass(annotation);
if (clazz == xmlSchemaClass) {
xmlAnn = annotation;
break;
}
}
}
if (xmlAnn == null) {
// XmlSchema annotation not found, let's add one
xmlAnn = p.annotate(xmlSchemaClass);
}
return xmlAnn;
}
@SuppressWarnings("unchecked")
private static Map<String, Integer> getUriCountMap(PackageOutline packageOutline) {
try {
final Field field = PackageOutlineImpl.class.getDeclaredField("uriCountMap");
field.setAccessible(true);
return (Map<String, Integer>) field.get(packageOutline);
}
catch (NoSuchFieldException e) {
throw new RuntimeException("Unable to access 'uriCountMap' field for package outline [" + packageOutline._package().name() + "] : " + e.getMessage(), e);
}
catch (IllegalAccessException e) {
throw new RuntimeException("Unable to find 'uriCountMap' field for package outline [" + packageOutline._package().name() + "] : " + e.getMessage(), e);
}
}
private static Model getPackageModel(PackageOutlineImpl packageOutline) {
try {
final Field field = PackageOutlineImpl.class.getDeclaredField("_model");
field.setAccessible(true);
return (Model) field.get(packageOutline);
}
catch (NoSuchFieldException e) {
throw new RuntimeException("Unable to access '_model' field for package outline [" + packageOutline._package().name() + "] : " + e.getMessage(), e);
}
catch (IllegalAccessException e) {
throw new RuntimeException("Unable to find '_model' field for package outline [" + packageOutline._package().name() + "] : " + e.getMessage(), e);
}
}
@SuppressWarnings("unchecked")
private static List<JAnnotationUse> getAnnotations(JPackage p) {
// TODO bump jaxb-xjc dependency to version >= 2.2.2 and use the annotations() method instead.
try {
final Field annotationsField = JPackage.class.getDeclaredField("annotations");
annotationsField.setAccessible(true);
return (List<JAnnotationUse>) annotationsField.get(p);
}
catch (IllegalAccessException e) {
throw new RuntimeException("Unable to access 'annotation' field for package [" + p.name() + "] : " + e.getMessage(), e);
}
catch (NoSuchFieldException e) {
throw new RuntimeException("Unable to find 'annotation' field for package [" + p.name() + "] : " + e.getMessage(), e);
}
}
private static JClass getAnnotationJClass(JAnnotationUse annotation) {
try {
final Field clazzField = JAnnotationUse.class.getDeclaredField("clazz");
clazzField.setAccessible(true);
return (JClass) clazzField.get(annotation);
}
catch (IllegalAccessException e) {
throw new RuntimeException("Unable to access 'clazz' field for class [JAnnotationUse] : " + e.getMessage(), e);
}
catch (NoSuchFieldException e) {
throw new RuntimeException("Unable to find 'annotation' field for class [JAnnotationUse] : " + e.getMessage(), e);
}
}
@SuppressWarnings("unchecked")
private static Map<String, JAnnotationValue> getAnnotationMemberValues(JAnnotationUse annotation) {
try {
final Field clazzField = JAnnotationUse.class.getDeclaredField("memberValues");
clazzField.setAccessible(true);
return (Map<String, JAnnotationValue>) clazzField.get(annotation);
}
catch (IllegalAccessException e) {
throw new RuntimeException("Unable to access 'memberValues' field for class [JAnnotationUse] : " + e.getMessage(), e);
}
catch (NoSuchFieldException e) {
throw new RuntimeException("Unable to find 'memberValues' field for class [JAnnotationUse] : " + e.getMessage(), e);
}
}
private static String getStringAnnotationValue(JAnnotationValue val) {
try {
final Field clazzField = val.getClass().getDeclaredField("value");
clazzField.setAccessible(true);
final JStringLiteral j = (JStringLiteral) clazzField.get(val);
return j == null ? null : j.str;
}
catch (IllegalAccessException e) {
throw new RuntimeException("Unable to access 'value' field for class [" + val.getClass() + "] : " + e.getMessage(), e);
}
catch (NoSuchFieldException e) {
throw new RuntimeException("Unable to find 'value' field for class [" + val.getClass() + "] : " + e.getMessage(), e);
}
}
private static class Pair {
private final String namespace;
private final String prefix;
private Pair(String namespace, String prefix) {
this.namespace = namespace;
this.prefix = prefix;
}
public String getNamespace() {
return namespace;
}
public String getPrefix() {
return prefix;
}
}
}