com.webcohesion.enunciate.modules.objc_client.ObjCXMLClientModule.java Source code

Java tutorial

Introduction

Here is the source code for com.webcohesion.enunciate.modules.objc_client.ObjCXMLClientModule.java

Source

/*
 * Copyright 2006-2008 Web Cohesion
 *
 * 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.webcohesion.enunciate.modules.objc_client;

import com.webcohesion.enunciate.EnunciateContext;
import com.webcohesion.enunciate.EnunciateException;
import com.webcohesion.enunciate.api.datatype.DataTypeReference;
import com.webcohesion.enunciate.api.resources.MediaTypeDescriptor;
import com.webcohesion.enunciate.api.resources.Method;
import com.webcohesion.enunciate.api.resources.Resource;
import com.webcohesion.enunciate.api.resources.ResourceGroup;
import com.webcohesion.enunciate.artifacts.ArtifactType;
import com.webcohesion.enunciate.artifacts.ClientLibraryArtifact;
import com.webcohesion.enunciate.artifacts.FileArtifact;
import com.webcohesion.enunciate.facets.FacetFilter;
import com.webcohesion.enunciate.module.*;
import com.webcohesion.enunciate.modules.jaxb.EnunciateJaxbContext;
import com.webcohesion.enunciate.modules.jaxb.JaxbModule;
import com.webcohesion.enunciate.modules.jaxb.api.impl.DataTypeReferenceImpl;
import com.webcohesion.enunciate.modules.jaxb.model.Attribute;
import com.webcohesion.enunciate.modules.jaxb.model.Element;
import com.webcohesion.enunciate.modules.jaxb.model.SchemaInfo;
import com.webcohesion.enunciate.modules.jaxb.model.TypeDefinition;
import com.webcohesion.enunciate.modules.jaxb.model.types.XmlClassType;
import com.webcohesion.enunciate.modules.jaxb.model.types.XmlType;
import com.webcohesion.enunciate.modules.jaxb.model.util.JAXBErrors;
import com.webcohesion.enunciate.modules.jaxb.model.util.MapType;
import com.webcohesion.enunciate.modules.jaxb.util.AccessorOverridesAnotherMethod;
import com.webcohesion.enunciate.modules.jaxb.util.FindRootElementMethod;
import com.webcohesion.enunciate.modules.jaxrs.JaxrsModule;
import com.webcohesion.enunciate.util.freemarker.FileDirective;
import com.webcohesion.enunciate.util.freemarker.IsFacetExcludedMethod;
import freemarker.cache.URLTemplateLoader;
import freemarker.core.Environment;
import freemarker.template.*;
import org.apache.commons.configuration.HierarchicalConfiguration;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Pattern;

/**
 * @author Ryan Heaton
 */
public class ObjCXMLClientModule extends BasicGeneratingModule implements ApiFeatureProviderModule {

    /**
     * The pattern to scrub is any non-word character.
     */
    private static final Pattern SCRUB_PATTERN = Pattern.compile("\\W");

    JaxbModule jaxbModule;
    JaxrsModule jaxrsModule;

    /**
     * @return "obj-c-xml-client"
     */
    @Override
    public String getName() {
        return "obj-c-xml-client";
    }

    @Override
    public List<DependencySpec> getDependencySpecifications() {
        return Arrays.asList((DependencySpec) new DependencySpec() {
            @Override
            public boolean accept(EnunciateModule module) {
                if (module instanceof JaxbModule) {
                    jaxbModule = (JaxbModule) module;
                    return true;
                } else if (module instanceof JaxrsModule) {
                    jaxrsModule = (JaxrsModule) module;
                    return true;
                }

                return module instanceof ApiRegistryProviderModule;
            }

            @Override
            public boolean isFulfilled() {
                return true;
            }

            @Override
            public String toString() {
                return "optional jaxb, optional jaxrs";
            }
        });
    }

    /**
     * Scrub a C identifier (removing any illegal characters, etc.).
     *
     * @param identifier The identifier.
     * @return The identifier.
     */
    public static String scrubIdentifier(String identifier) {
        return identifier == null ? null : SCRUB_PATTERN.matcher(identifier).replaceAll("_");
    }

    @Override
    public void call(EnunciateContext context) {
        if (this.jaxbModule == null || this.jaxbModule.getJaxbContext() == null
                || this.jaxbModule.getJaxbContext().getSchemas().isEmpty()) {
            info("No JAXB XML data types: Objective-C XML client will not be generated.");
            return;
        }

        if (usesUnmappableElements()) {
            warn("Web service API makes use of elements that cannot be handled by the Objective-C XML client. Objective-C XML client will not be generated.");
            return;
        }

        List<String> namingConflicts = JAXBErrors
                .findConflictingAccessorNamingErrors(this.jaxbModule.getJaxbContext());
        if (namingConflicts != null && !namingConflicts.isEmpty()) {
            error("JAXB naming conflicts have been found:");
            for (String namingConflict : namingConflicts) {
                error(namingConflict);
            }
            error("These naming conflicts are often between the field and it's associated property, in which case you need to use one or two of the following strategies to avoid the conflicts:");
            error("1. Explicitly exclude one or the other.");
            error("2. Put the annotations on the property instead of the field.");
            error("3. Tell JAXB to use a different process for detecting accessors using the @XmlAccessorType annotation.");
            throw new EnunciateException("JAXB naming conflicts detected.");
        }

        EnunciateJaxbContext jaxbContext = this.jaxbModule.getJaxbContext();

        Map<String, String> packageIdentifiers = getPackageIdentifiers();

        String packageIdentifierPattern = getPackageIdentifierPattern();
        if ((packageIdentifierPattern != null)) {
            for (SchemaInfo schemaInfo : jaxbContext.getSchemas().values()) {
                for (TypeDefinition typeDefinition : schemaInfo.getTypeDefinitions()) {
                    String pckg = typeDefinition.getPackage().getQualifiedName().toString();
                    if (!packageIdentifiers.containsKey(pckg)) {
                        try {
                            packageIdentifiers.put(pckg,
                                    String.format(packageIdentifierPattern, pckg.split("\\.", 9)));
                        } catch (IllegalFormatException e) {
                            warn("Unable to format package %s with format pattern %s (%s)", pckg,
                                    packageIdentifierPattern, e.getMessage());
                        }
                    }
                }
            }
        }

        Map<String, Object> model = new HashMap<String, Object>();

        String slug = getSlug();

        model.put("slug", slug);

        File srcDir = getSourceDir();

        TreeMap<String, String> translations = new TreeMap<String, String>();
        translations.put("id", getTranslateIdTo());
        model.put("clientSimpleName", new ClientSimpleNameMethod(translations));

        List<TypeDefinition> schemaTypes = new ArrayList<TypeDefinition>();
        ExtensionDepthComparator comparator = new ExtensionDepthComparator();
        for (SchemaInfo schemaInfo : jaxbContext.getSchemas().values()) {
            for (TypeDefinition typeDefinition : schemaInfo.getTypeDefinitions()) {
                int position = Collections.binarySearch(schemaTypes, typeDefinition, comparator);
                if (position < 0) {
                    position = -position - 1;
                }
                schemaTypes.add(position, typeDefinition);
            }
        }
        model.put("schemaTypes", schemaTypes);

        NameForTypeDefinitionMethod nameForTypeDefinition = new NameForTypeDefinitionMethod(
                getTypeDefinitionNamePattern(), slug, jaxbContext.getNamespacePrefixes(), packageIdentifiers);
        model.put("nameForTypeDefinition", nameForTypeDefinition);
        model.put("nameForEnumConstant", new NameForEnumConstantMethod(getEnumConstantNamePattern(), slug,
                jaxbContext.getNamespacePrefixes(), packageIdentifiers));
        TreeMap<String, String> conversions = new TreeMap<String, String>();
        for (SchemaInfo schemaInfo : jaxbContext.getSchemas().values()) {
            for (TypeDefinition typeDefinition : schemaInfo.getTypeDefinitions()) {
                if (typeDefinition.isEnum()) {
                    conversions.put(typeDefinition.getQualifiedName().toString(),
                            "enum " + nameForTypeDefinition.calculateName(typeDefinition));
                } else {
                    conversions.put(typeDefinition.getQualifiedName().toString(),
                            (String) nameForTypeDefinition.calculateName(typeDefinition));
                }
            }
        }
        ClientClassnameForMethod classnameFor = new ClientClassnameForMethod(conversions, jaxbContext);
        model.put("classnameFor", classnameFor);
        model.put("functionIdentifierFor", new FunctionIdentifierForMethod(nameForTypeDefinition, jaxbContext));
        model.put("objcBaseName", slug);
        model.put("separateCommonCode", isSeparateCommonCode());
        model.put("findRootElement", new FindRootElementMethod(jaxbContext));
        model.put("referencedNamespaces", new ReferencedNamespacesMethod(jaxbContext));
        model.put("prefix", new PrefixMethod(jaxbContext.getNamespacePrefixes()));
        model.put("accessorOverridesAnother", new AccessorOverridesAnotherMethod());
        model.put("file", new FileDirective(srcDir, this.enunciate.getLogger()));

        Set<String> facetIncludes = new TreeSet<String>(this.enunciate.getConfiguration().getFacetIncludes());
        facetIncludes.addAll(getFacetIncludes());
        Set<String> facetExcludes = new TreeSet<String>(this.enunciate.getConfiguration().getFacetExcludes());
        facetExcludes.addAll(getFacetExcludes());
        FacetFilter facetFilter = new FacetFilter(facetIncludes, facetExcludes);

        model.put("isFacetExcluded", new IsFacetExcludedMethod(facetFilter));

        if (!isUpToDateWithSources(srcDir)) {
            debug("Generating the C data structures and (de)serialization functions...");
            URL apiTemplate = getTemplateURL("api.fmt");
            try {
                processTemplate(apiTemplate, model);
            } catch (IOException e) {
                throw new EnunciateException(e);
            } catch (TemplateException e) {
                throw new EnunciateException(e);
            }
        } else {
            info("Skipping C code generation because everything appears up-to-date.");
        }

        ClientLibraryArtifact artifactBundle = new ClientLibraryArtifact(getName(), "objc.client.library",
                "Objective C Client Library");
        FileArtifact sourceHeader = new FileArtifact(getName(), "objc.client.h", new File(srcDir, slug + ".h"));
        sourceHeader.setPublic(false);
        sourceHeader.setArtifactType(ArtifactType.sources);
        FileArtifact sourceImpl = new FileArtifact(getName(), "objc.client.m", new File(srcDir, slug + ".m"));
        sourceImpl.setPublic(false);
        sourceImpl.setArtifactType(ArtifactType.sources);
        String description = readResource("library_description.fmt", model, nameForTypeDefinition); //read in the description from file
        artifactBundle.setDescription(description);
        artifactBundle.addArtifact(sourceHeader);
        artifactBundle.addArtifact(sourceImpl);
        if (isSeparateCommonCode()) {
            FileArtifact commonSourceHeader = new FileArtifact(getName(), "objc.common.client.h",
                    new File(srcDir, "enunciate-common.h"));
            commonSourceHeader.setPublic(false);
            commonSourceHeader.setArtifactType(ArtifactType.sources);
            commonSourceHeader.setDescription("Common header needed for all projects.");
            FileArtifact commonSourceImpl = new FileArtifact(getName(), "objc.common.client.m",
                    new File(srcDir, "enunciate-common.m"));
            commonSourceImpl.setPublic(false);
            commonSourceImpl.setArtifactType(ArtifactType.sources);
            commonSourceImpl.setDescription("Common implementation code needed for all projects.");
            artifactBundle.addArtifact(commonSourceHeader);
            artifactBundle.addArtifact(commonSourceImpl);
        }
        this.enunciate.addArtifact(artifactBundle);
    }

    protected boolean usesUnmappableElements() {
        boolean usesUnmappableElements = false;

        if (this.jaxbModule != null && this.jaxbModule.getJaxbContext() != null
                && !this.jaxbModule.getJaxbContext().getSchemas().isEmpty()) {
            for (SchemaInfo schemaInfo : this.jaxbModule.getJaxbContext().getSchemas().values()) {
                for (TypeDefinition complexType : schemaInfo.getTypeDefinitions()) {
                    for (Attribute attribute : complexType.getAttributes()) {
                        if (attribute.isXmlList()) {
                            info("%s: The Objective-C client code won't serialize xml lists as an array, instead passing the list as a string that will need to be parsed. This may cause confusion to C consumers.",
                                    positionOf(attribute));
                        }

                        if (attribute.isCollectionType() && attribute.isBinaryData()) {
                            warn("%s: The Objective-C client code doesn't support a collection of items that are binary data. You'll have to define separate accessors for each item or disable the C module.",
                                    positionOf(attribute));
                            usesUnmappableElements = true;
                        }
                    }

                    if (complexType.getValue() != null) {
                        if (complexType.getValue().isXmlList()) {
                            info("%s: The Objective-C client code won't serialize xml lists as an array, instead passing the list as a string that will need to be parsed. This may cause confusion to C consumers.",
                                    positionOf(complexType.getValue()));
                        }

                        if (complexType.getValue().isCollectionType() && complexType.getValue().isBinaryData()) {
                            warn("%s: The Objective-C client code doesn't support a collection of items that are binary data.",
                                    positionOf(complexType.getValue()));
                            usesUnmappableElements = true;
                        }
                    }

                    for (Element element : complexType.getElements()) {
                        if (element.isXmlList()) {
                            info("%s: The Objective-C client code won't serialize xml lists as an array, instead passing the list as a string that will need to be parsed. This may cause confusion to C consumers.",
                                    positionOf(element));
                        }

                        if (element.getAccessorType() instanceof MapType && !element.isAdapted()) {
                            warn("%s: The Objective-C client doesn't have a built-in way of serializing a Map. Use @XmlJavaTypeAdapter to supply your own adapter for the Map.",
                                    positionOf(element));
                            usesUnmappableElements = true;
                        }

                        if (element.isCollectionType()) {
                            if (element.getChoices().size() > 1) {
                                info("%s: The Objective-C client code doesn't fully support multiple choices for a collection. It has to separate each choice into its own array. "
                                        + "This makes the C API a bit awkward to use and makes it impossible to preserve the order of the collection. If order is relevant, consider breaking out "
                                        + "the choices into their own collection or otherwise refactoring the API.",
                                        positionOf(element));
                            }

                            if (element.isBinaryData()) {
                                warn("%s: The Objective-C client code doesn't support a collection of items that are binary data.",
                                        positionOf(element));
                                usesUnmappableElements = true;
                            }

                            for (Element choice : element.getChoices()) {
                                if (choice.isNillable()) {
                                    info("%s: The Objective-C client code doesn't support nillable items in a collection (the nil items will be skipped). This may cause confusion to C consumers.",
                                            positionOf(choice));
                                }
                            }
                        }
                    }
                }
            }
        }

        return usesUnmappableElements;
    }

    protected File getSourceDir() {
        return new File(new File(this.enunciate.getBuildDir(), getName()), "src");
    }

    /**
     * Processes the specified template with the given model.
     *
     * @param templateURL The template URL.
     * @param model       The root model.
     */
    public String processTemplate(URL templateURL, Object model) throws IOException, TemplateException {
        debug("Processing template %s.", templateURL);
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_22);

        configuration.setTemplateLoader(new URLTemplateLoader() {
            protected URL getURL(String name) {
                try {
                    return new URL(name);
                } catch (MalformedURLException e) {
                    return null;
                }
            }
        });

        configuration.setTemplateExceptionHandler(new TemplateExceptionHandler() {
            public void handleTemplateException(TemplateException templateException, Environment environment,
                    Writer writer) throws TemplateException {
                throw templateException;
            }
        });

        configuration.setLocalizedLookup(false);
        configuration.setDefaultEncoding("UTF-8");
        configuration.setObjectWrapper(new ObjCXMLClientObjectWrapper());
        Template template = configuration.getTemplate(templateURL.toString());
        StringWriter unhandledOutput = new StringWriter();
        template.process(model, unhandledOutput);
        unhandledOutput.close();
        return unhandledOutput.toString();
    }

    /**
     * Reads a resource into string form.
     *
     * @param resource The resource to read.
     * @return The string form of the resource.
     */
    protected String readResource(String resource, Map<String, Object> model,
            NameForTypeDefinitionMethod nameForTypeDefinition) {
        Method exampleResource = findExampleResourceMethod();

        if (exampleResource != null) {
            TypeDefinition typeDefinition = findRequestElement(exampleResource);
            if (typeDefinition != null) {
                model.put("input_element_name", nameForTypeDefinition.calculateName(typeDefinition));
            }

            typeDefinition = findResponseElement(exampleResource);
            if (typeDefinition != null) {
                model.put("output_element_name", nameForTypeDefinition.calculateName(typeDefinition));
            }

            model.put("resource_url", exampleResource.getResource().getPath());
            model.put("resource_method", exampleResource.getHttpMethod());
        }

        URL res = ObjCXMLClientModule.class.getResource(resource);
        try {
            return processTemplate(res, model);
        } catch (TemplateException e) {
            throw new EnunciateException(e);
        } catch (IOException e) {
            throw new EnunciateException(e);
        }

    }

    private TypeDefinition findRequestElement(Method exampleResource) {
        if (exampleResource.getRequestEntity() != null) {
            for (MediaTypeDescriptor mediaTypeDescriptor : exampleResource.getRequestEntity().getMediaTypes()) {
                if (EnunciateJaxbContext.SYNTAX_LABEL.equals(mediaTypeDescriptor.getSyntax())) {
                    DataTypeReference dataType = mediaTypeDescriptor.getDataType();
                    if (dataType instanceof DataTypeReferenceImpl) {
                        XmlType xmlType = ((DataTypeReferenceImpl) dataType).getXmlType();
                        if (xmlType instanceof XmlClassType) {
                            return ((XmlClassType) xmlType).getTypeDefinition();
                        }
                    }
                }
            }
        }
        return null;
    }

    private TypeDefinition findResponseElement(Method exampleResource) {
        if (exampleResource.getResponseEntity() != null) {
            for (MediaTypeDescriptor mediaTypeDescriptor : exampleResource.getResponseEntity().getMediaTypes()) {
                if (EnunciateJaxbContext.SYNTAX_LABEL.equals(mediaTypeDescriptor.getSyntax())) {
                    DataTypeReference dataType = mediaTypeDescriptor.getDataType();
                    if (dataType instanceof DataTypeReferenceImpl) {
                        XmlType xmlType = ((DataTypeReferenceImpl) dataType).getXmlType();
                        if (xmlType instanceof XmlClassType) {
                            return ((XmlClassType) xmlType).getTypeDefinition();
                        }
                    }
                }
            }
        }
        return null;
    }

    /**
     * Finds an example resource method, according to the following preference order:
     *
     * <ol>
     * <li>The first method annotated with {@link com.webcohesion.enunciate.metadata.DocumentationExample}.
     * <li>The first method with BOTH an output payload with a known XML element and an input payload with a known XML element.
     * <li>The first method with an output payload with a known XML element.
     * </ol>
     *
     * @return An example resource method, or if no good examples were found.
     */
    public Method findExampleResourceMethod() {
        Method example = null;
        List<ResourceGroup> resourceGroups = this.jaxrsModule.getJaxrsContext().getResourceGroups();
        for (ResourceGroup resourceGroup : resourceGroups) {
            List<Resource> resources = resourceGroup.getResources();
            for (Resource resource : resources) {
                for (Method method : resource.getMethods()) {
                    if (hasXmlResponseEntity(method)) {
                        if (hasXmlRequestEntity(method)) {
                            //we'll prefer one with both an output AND an input.
                            return method;
                        } else {
                            //we'll prefer the first one we find with an output.
                            example = example == null ? method : example;
                        }
                    }
                }
            }
        }

        return example;
    }

    private boolean hasXmlResponseEntity(Method method) {
        if (method.getResponseEntity() != null) {
            for (MediaTypeDescriptor mediaTypeDescriptor : method.getResponseEntity().getMediaTypes()) {
                if (EnunciateJaxbContext.SYNTAX_LABEL.equals(mediaTypeDescriptor.getSyntax())) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean hasXmlRequestEntity(Method method) {
        if (method.getRequestEntity() != null) {
            for (MediaTypeDescriptor mediaTypeDescriptor : method.getRequestEntity().getMediaTypes()) {
                if (EnunciateJaxbContext.SYNTAX_LABEL.equals(mediaTypeDescriptor.getSyntax())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Get a template URL for the template of the given name.
     *
     * @param template The specified template.
     * @return The URL to the specified template.
     */
    protected URL getTemplateURL(String template) {
        return ObjCXMLClientModule.class.getResource(template);
    }

    /**
     * The label for the Ruby API.
     *
     * @return The label for the Ruby API.
     */
    public String getSlug() {
        return this.config.getString("[@slug]", this.enunciate.getConfiguration().getSlug());
    }

    /**
     * The package-to-module conversions.
     *
     * @return The package-to-module conversions.
     */
    public Map<String, String> getPackageIdentifiers() {
        List<HierarchicalConfiguration> conversionElements = this.config.configurationsAt("package");
        HashMap<String, String> conversions = new HashMap<String, String>();
        for (HierarchicalConfiguration conversionElement : conversionElements) {
            conversions.put(conversionElement.getString("[@name]"), conversionElement.getString("[@identifier]"));
        }
        return conversions;
    }

    /**
     * The format string creating a package identifier from a package name.
     *
     * @return The format string creating a package identifier from a package name.
     */
    public String getPackageIdentifierPattern() {
        return this.config.getString("[@packageIdentifierPattern]", null);
    }

    /**
     * The pattern for converting a type definition to a unique C-style type name.
     *
     * @return The pattern for converting a type definition to a unique C-style type name.
     */
    public String getTypeDefinitionNamePattern() {
        return this.config.getString("[@typeDefinitionNamePattern]", "%1$S%2$S%4$s");
    }

    /**
     * The pattern for converting an enum constant to a unique C-style type name.
     *
     * @return The pattern for converting an enum constant to a unique C-style type name.
     */
    public String getEnumConstantNamePattern() {
        return this.config.getString("[@enumConstantNamePattern]", "%1$S_%2$S_%3$S_%9$S");
    }

    /**
     * What to translate 'id' to when writing out objective-c code.
     *
     * @return What to translate 'id' to when writing out objective-c code.
     */
    public String getTranslateIdTo() {
        return this.config.getString("[@translateIdTo]", "identifier");
    }

    /**
     * Whether to separate the common code from the project-specific code.
     *
     * @return Whether to separate the common code from the project-specific code.
     */
    public boolean isSeparateCommonCode() {
        return this.config.getBoolean("[@separateCommonCode]", true);
    }

    public Set<String> getFacetIncludes() {
        List<Object> includes = this.config.getList("facets.include[@name]");
        Set<String> facetIncludes = new TreeSet<String>();
        for (Object include : includes) {
            facetIncludes.add(String.valueOf(include));
        }
        return facetIncludes;
    }

    public Set<String> getFacetExcludes() {
        List<Object> excludes = this.config.getList("facets.exclude[@name]");
        Set<String> facetExcludes = new TreeSet<String>();
        for (Object exclude : excludes) {
            facetExcludes.add(String.valueOf(exclude));
        }
        return facetExcludes;
    }

    private static final class ExtensionDepthComparator implements Comparator<TypeDefinition> {
        public int compare(TypeDefinition t1, TypeDefinition t2) {
            int depth1 = 0;
            int depth2 = 0;

            XmlType superType = t1.getBaseType();
            while (superType instanceof XmlClassType) {
                depth1++;
                superType = ((XmlClassType) superType).getTypeDefinition().getBaseType();
            }

            superType = t2.getBaseType();
            while (superType instanceof XmlClassType) {
                depth2++;
                superType = ((XmlClassType) superType).getTypeDefinition().getBaseType();
            }

            return depth1 - depth2;
        }
    }
}