All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.massfords.jaxb.AttributeInitializerPlugin Maven / Gradle / Ivy

package com.massfords.jaxb;

import com.sun.codemodel.JFieldVar;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.model.CAttributePropertyInfo;
import com.sun.tools.xjc.model.CCustomizations;
import com.sun.tools.xjc.model.CPluginCustomization;
import com.sun.tools.xjc.model.CPropertyInfo;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.FieldOutline;
import com.sun.tools.xjc.outline.Outline;
import org.jvnet.jaxb2_commons.util.CustomizationUtils;
import org.jvnet.jaxb2_commons.util.FieldAccessorUtils;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.namespace.QName;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Fast and dirty JAXB plugin to add an attribute initializer to attributes that
 * have a default value but only get the default behavior provided when you call
 * the getter. This may be fine where you have JAXB or Schema aware parties on
 * both ends of the wire but in cases where you're going from JAXB to JSON it's
 * nice to provide all of the default values since the recipient may be worlds
 * away from having access to a Schema and would instead prefer to be told what
 * the default values for fields are.
 *
 * I had assumed that this was a solved problem but here's what I found:
 * - XJC behavior for default attributes is to provide the default value through
 * the getter. This is fine but if you're marshalling via the XmlFieldAccessor
 * then this doesn't help.
 * - There's a JAXB Default Value plugin but this focuses on elements since
 * attributes are supposedly covered.
 *
 * @author markford
 */
public class AttributeInitializerPlugin extends Plugin {
    /**
     * Namespace value that we look for in the schema
     */
    private static final String NAMESPACE_URI = "urn:com.massfords.jaxb";

    /**
     * The one element that we know how to process
     */
    private static final QName ATTR_INIT_QNAME = new QName(NAMESPACE_URI, "attrinit");

    /**
     * This is the flag to trigger the behavior
     */
    @Override
    public String getOptionName() {
        return "Xattrinit";
    }

    @Override
    public String getUsage() {
        return null;
    }

    @Override
    public boolean run(Outline outline, Options options, ErrorHandler errorHandler) throws SAXException {

        for (ClassOutline classOutline : outline.getClasses()) {
            processClassOutline(outline, classOutline, errorHandler);
        }
        return true;
    }

    private void processClassOutline(Outline outline, ClassOutline classOutline,
                                       ErrorHandler errorHandler) throws SAXException {

        for (FieldOutline fieldOutline : classOutline.getDeclaredFields()) {
            processFieldOutline(outline, fieldOutline, errorHandler);
        }
    }

    private void processFieldOutline(Outline outline, FieldOutline fieldOutline,
                                     ErrorHandler errorHandler) throws SAXException {
        // Get all of the customizations attached to this field
        final CCustomizations customizations =
                CustomizationUtils.getCustomizations(fieldOutline);

        // Look to see if it has any of the ones we're looking to handle
        List ourCustomizations = customizations.stream()
                .filter(cPluginCustomization ->
                        ATTR_INIT_QNAME.getNamespaceURI().equals(cPluginCustomization.element.getNamespaceURI()) &&
                                ATTR_INIT_QNAME.getLocalPart().equals(cPluginCustomization.element.getLocalName()))
                .collect(Collectors.toList());

        if (!ourCustomizations.isEmpty()) {

            // mark them as acknowledged
            ourCustomizations.forEach(CPluginCustomization::markAsAcknowledged);

            // warn the user if they've provided more than one customization
            // element. We're only going to process the first one in the list
            if (ourCustomizations.size()>1) {
                ourCustomizations.subList(1, ourCustomizations.size()).forEach(
                        c->warn("ignoring extra attrinit elements", errorHandler, c.locator)
                );
            }

            // used to report the location of an error
            Locator locator = ourCustomizations.get(0).locator;

            // get a reference to the field in question. This should never be
            // null but I'll check anyway and print a warning if it is
            final JFieldVar field = FieldAccessorUtils.field(fieldOutline);
            if (field != null) {
                CPropertyInfo propInfo = fieldOutline.getPropertyInfo();

                if (propInfo instanceof CAttributePropertyInfo) {
                    CAttributePropertyInfo attribPropInfo = (CAttributePropertyInfo) propInfo;
                    if (attribPropInfo.defaultValue != null) {
                        field.init(attribPropInfo.defaultValue.compute(outline));
                    } else {
                        fatal("extension element should only appear on attribute declarations with default value",
                                errorHandler, locator);
                    }
                } else {
                    fatal("extension element should only appear on attribute declarations",
                            errorHandler, locator);
                }
            } else {
                fatal("was expecting to find a field but didn't", errorHandler, locator);
            }
        } // no need for an else here. It's not a schema particle we're interested in
    }

    /**
     * Odd that the ErrorHandler has a checked exception when trying to report an error.
     * @param message
     * @param errorHandler
     * @param locator
     */
    private void warn(String message, ErrorHandler errorHandler, Locator locator) {
        try {
            errorHandler.warning(new SAXParseException(message, locator, null));
        } catch (SAXException e) {
            // ignore any exceptions while we're trying to warn the user
        }
    }

    /**
     * Reports a fatal exception to the error handler. These are conditions that
     * the user should be aware of because they've configured our plugin but
     * have configured it incorrectly.
     * @param message
     * @param errorHandler
     * @param locator
     * @throws SAXException
     */
    private void fatal(String message, ErrorHandler errorHandler, Locator locator) throws SAXException {
        errorHandler.fatalError(new SAXParseException(message, locator, null));
    }

    /**
     * Returns all of the namespaces we're looking to handle
     * @return
     */
    @Override
    public List getCustomizationURIs() {
        return Collections.singletonList(NAMESPACE_URI);
    }

    /**
     * Returns true if it's an element we know how to handle
     * @param nsUri
     * @param localName
     * @return
     */
    @Override
    public boolean isCustomizationTagName(String nsUri, String localName) {
        return NAMESPACE_URI.equals(nsUri) && ATTR_INIT_QNAME.getLocalPart().equals(localName);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy