CodeGenTest.java

/*
 * Copyright (c) 2021, 2024 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package com.sun.tools.xjc;

import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDeclaration;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JFormatter;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JType;
import com.sun.codemodel.writer.SingleStreamCodeWriter;
import com.sun.tools.xjc.api.ErrorListener;
import com.sun.tools.xjc.api.S2JJAXBModel;
import com.sun.tools.xjc.api.SchemaCompiler;
import com.sun.tools.xjc.api.XJC;
import com.sun.tools.xjc.util.Util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

import junit.framework.TestCase;
import org.junit.Assert;
import org.xml.sax.InputSource;

/**
 * Test code generated by the {@link SchemaCompiler}.
 *
 * @author lukas
 */
public class CodeGenTest extends TestCase {

    // See com.sun.tools.xjc.api.impl.s2j.SchemaCompilerImpl
    protected static final String XML_API_TEST = "xjc-api.test";

    public void testGh1460_Gh1064() throws Throwable {
        SchemaCompiler sc = XJC.createSchemaCompiler();
        sc.forcePackageName("ghbugs.b1460");
        sc.parseSchema(getInputSource("/schemas/ghbugs.xsd"));
        S2JJAXBModel model = sc.bind();
        JCodeModel code = model.generateCode(null, null);
        String method = toString(code._getClass("ghbugs.b1460.Gh1460Type").getMethod("setBinaryAttr", new JType[]{code.parseType("byte[][]")}));
//        com.sun.tools.xjc.api.Driver.dumpCode(code);
//        System.out.println(method);

        // wrong initialization of multi-dim array
        // https://github.com/eclipse-ee4j/jaxb-ri/issues/1460
        assertTrue(method, method.contains("new byte[len][]"));

        // null check in setter
        // https://github.com/eclipse-ee4j/jaxb-ri/issues/1064
        assertTrue(method, method.contains("if (values == null) {"));
        assertTrue(method, method.contains("this.binaryAttr = null;"));
        assertTrue(method, method.contains("return ;"));
    }

    public void testGh1460_Gh1064_jaxb21() throws Throwable {
        SchemaCompiler sc = XJC.createSchemaCompiler();
        sc.setErrorListener(new ConsoleErrorReporter());
        sc.forcePackageName("ghbugs.b1460.jaxb21");
        sc.parseSchema(getInputSource("/schemas/ghbugs-jaxb21.xsd"));
        S2JJAXBModel model = sc.bind();
        JCodeModel code = model.generateCode(null, null);
        String method = toString(code._getClass("ghbugs.b1460.jaxb21.Gh1460Type").getMethod("setBinaryAttr", new JType[]{code.parseType("byte[][]")}));
//        com.sun.tools.xjc.api.Driver.dumpCode(code);
//        System.out.println(method);

        // wrong initialization of multi-dim array
        // https://github.com/eclipse-ee4j/jaxb-ri/issues/1460
        assertTrue(method, method.contains("new byte[len][]"));

        // null check in setter
        // https://github.com/eclipse-ee4j/jaxb-ri/issues/1064
        assertTrue(method, method.contains("if (values == null) {"));
        assertTrue(method, method.contains("this.binaryAttr = null;"));
        assertTrue(method, method.contains("return ;"));
    }

    /**
     * Test issues #1748 and #1750 for {@code com.sun.tools.xjc.generator.bean.ElementOutlineImpl}.
     *
     * @throws FileNotFoundException When the test schema file cannot be read.
     * @throws URISyntaxException When the test {@link InputSource} cannot be parsed.
     *
     * @see <a href="https://github.com/eclipse-ee4j/jaxb-ri/issues/1748">Issue #1748</a>
     * @see <a href="https://github.com/eclipse-ee4j/jaxb-ri/issues/1750">Issue #1750</a>
     */
    public void testIssue1750() throws FileNotFoundException, URISyntaxException {
        String schemaFileName = "/schemas/issue1750/schema.xsd";
        String packageName = "org.example.issue1750";
        String someClassName = packageName + ".SomeJAXBElement";
        String suidFieldName = "serialVersionUID";

        // Parse the XML schema.
        SchemaCompiler sc = XJC.createSchemaCompiler();
        sc.forcePackageName(packageName);
        sc.parseSchema(getInputSource(schemaFileName));

        // Generate the defined class.
        S2JJAXBModel model = sc.bind();
        Plugin[] extensions = null;
        ErrorListener errorListener = new ConsoleErrorReporter();
        JCodeModel cm = model.generateCode(extensions, errorListener);
        JDefinedClass dc = cm._getClass(someClassName);
        assertNotNull(someClassName, dc);

        // Assert serialVersionUID
        assertTrue(suidFieldName, dc.fields().containsKey(suidFieldName));
        assertNotNull(suidFieldName + " value", dc.fields().get(suidFieldName));

        // Assert Class includes narrow type
        Iterator<JMethod> conIter = dc.constructors();
        while (conIter.hasNext()) {
            JMethod con = conIter.next();
            assertTrue(toString(con).contains("java.lang.Class<java.lang.String>"));
        }
    }

    /**
     * Test issues #1785 for {@link com.sun.tools.xjc.generator.bean.field.ConstField}.
     *
     * @throws FileNotFoundException When the test schema file cannot be read.
     * @throws URISyntaxException When the test {@link InputSource} cannot be parsed.
     *
     * @see <a href="https://github.com/eclipse-ee4j/jaxb-ri/issues/1785">Issue #1785</a>
     */
    public void testIssue1785() throws FileNotFoundException, URISyntaxException, IOException {
        String schemaFileName = "/schemas/issue1785/document.xsd";
        String packageName = "org.example.issue1785";
        String documentName = packageName + ".Document";
        String suidFieldName = "serialVersionUID";
        String codeModelDestPathName = "target/generated-test-sources/issue1785";

        // Parse the XML schema.
        SchemaCompiler sc = XJC.createSchemaCompiler();
        sc.forcePackageName(packageName);
        sc.parseSchema(getInputSource(schemaFileName));

        // Generate the defined model.
        S2JJAXBModel model = sc.bind();
        Plugin[] extensions = null;
        ErrorListener errorListener = new ConsoleErrorReporter();
        JCodeModel cm = model.generateCode(extensions, errorListener);

        // Assert Document class is modeled.
        JDefinedClass dc = cm._getClass(documentName);
        assertNotNull(documentName, dc);

        // Assert serialVersionUID
        assertTrue(suidFieldName, dc.fields().containsKey(suidFieldName));
        assertNotNull(suidFieldName + " value", dc.fields().get(suidFieldName));

        // Generate the Document classes to a directory, for review.
        if ( Util.getSystemProperty(XML_API_TEST) != null ) {
            File codeModelDestPath = new File(codeModelDestPathName);
            if ( !codeModelDestPath.exists() )
                codeModelDestPath.mkdirs();
            cm.build(codeModelDestPath);
        }

        // Generate the Document classes to single String, for assertions.
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        cm.build(new SingleStreamCodeWriter(baos));
        String cmString = baos.toString(StandardCharsets.UTF_8);

        // Assert non-empty javadoc blocks.
        assertNonEmptyJavadocBlocks(cmString);
    }

    private void assertNonEmptyJavadocBlocks(String cmString) throws IOException {
        int lineNo = 0;
        try ( LineNumberReader lnr = new LineNumberReader(new StringReader(cmString)) ) {
            StringBuilder javadocBlock = null;
            String line;
            while ( (line = lnr.readLine()) != null ) {
                String trimLine = line.trim();
                String javadoc = null;
                if ( trimLine.startsWith("/**") ) {
                    lineNo = lnr.getLineNumber();
                    javadocBlock = new StringBuilder();
                    javadoc = trimLine.substring(3);
                }
                if ( javadocBlock != null ) {
                    if ( javadoc == null ) {
                        if ( trimLine.startsWith("*") && !trimLine.startsWith("*/") )
                            javadoc = trimLine.substring(1);
                        else
                            javadoc = trimLine;
                    }
                    int endIndex = 0;
                    if ( (endIndex = javadoc.lastIndexOf("*/")) != -1 ) {
                        javadocBlock.append(javadoc.substring(0, endIndex));
                        int javadocLen = javadocBlock.toString().trim().length();

                        // Assert current javadoc block length is not zero!
                        Assert.assertNotEquals("Empty javadoc at " + lineNo, 0, javadocLen);
                        javadocBlock = null;
                    }
                    else
                        javadocBlock.append(javadoc);
                }
            }
        }
    }

    private InputSource getInputSource(String systemId) throws FileNotFoundException, URISyntaxException {
        URL url = CodeGenTest.class.getResource(systemId);
        File f = new File(url.toURI());
        InputSource is = new InputSource(new FileInputStream(f));
        is.setSystemId(f.toURI().toString());
        return is;
    }

    private static String toString(JDeclaration generable) {
        if (generable == null) {
            throw new IllegalArgumentException("Generable must not be null.");
        }
        final StringWriter stringWriter = new StringWriter();
        final JFormatter formatter = new JFormatter(stringWriter);
        generable.declare(formatter);
        return stringWriter.toString();
    }
}