/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   https://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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

// Package
///////////////
package jena;


import static org.apache.jena.atlas.logging.LogCtl.setLogging;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.jena.ontology.Individual;
import org.apache.jena.ontology.OntModel;
import org.apache.jena.ontology.OntModelSpec;
import org.apache.jena.rdf.model.*;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RDFFormat;
import org.apache.jena.riot.RDFWriter;
import org.apache.jena.shared.JenaException;
import org.apache.jena.util.XML10Char;
import org.apache.jena.util.iterator.ExtendedIterator;
import org.apache.jena.util.iterator.WrappedIterator;
import org.apache.jena.vocabulary.OWL;
import org.apache.jena.vocabulary.RDF;
import org.apache.jena.vocabulary.RDFS;
import org.apache.jena.vocabulary.XSD;

/**
 * <p>
 * A vocabulary generator, that will consume an ontology or other vocabulary file,
 * and generate a Java file with the constants from the vocabulary compiled in.
 * Designed to be highly flexible and customisable.
 * </p>
 */
@SuppressWarnings("removal")
public class schemagen {

    static { setLogging(); }

    // Constants
    //////////////////////////////////

    /** The namespace for the configuration model is {@value} */
    public static final String NS = "http://jena.hpl.hp.com/2003/04/schemagen#";

    /** The default location of the configuration model is {@value} */
    public static final String DEFAULT_CONFIG_URI = "file:schemagen.rdf";

    /** The default marker string for denoting substitutions is {@value} */
    public static final String DEFAULT_MARKER = "%";

    /** Default template for writing out value declarations */
    public static final String DEFAULT_TEMPLATE = "public static final %valclass% %valname% = M_MODEL.%valcreator%( \"%valuri%\" );";

    /** Default template for writing out individual declarations */
    public static final String DEFAULT_INDIVIDUAL_TEMPLATE = "public static final %valclass% %valname% = M_MODEL.%valcreator%( \"%valuri%\", %valtype% );";

    /** Default template for writing out individual declarations for non-ontology vocabularies */
    public static final String DEFAULT_RDFS_INDIVIDUAL_TEMPLATE = "public static final %valclass% %valname% = M_MODEL.%valcreator%( \"%valuri%\" );";

    /** Default template for the file header */
    public static final String DEFAULT_HEADER_TEMPLATE = "/* CVS $" + "Id: $ */%nl%%package% %nl%%imports% %nl%/**%nl% * Vocabulary definitions from %sourceURI% %nl% * @author Auto-generated by schemagen on %date% %nl% */";

    /** Default line length for comments before wrap */
    public static final int COMMENT_LENGTH_LIMIT = 80;


    /** List of Java reserved keywords, see <a href="http://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html">this list</a>. */
    public static final String[] JAVA_KEYWORDS = {
        "abstract",    "continue",    "for",         "new",         "switch",
        "assert",      "default",     "goto",        "package",     "synchronized",
        "boolean",     "do",          "if",          "private",     "this",
        "break",       "double",      "implements",  "protected",   "throw",
        "byte",        "else",        "import",      "public",      "throws",
        "case",        "enum",        "instanceof",  "return",      "transient",
        "catch",       "extends",     "int",         "short",       "try",
        "char",        "final",       "interface",   "static",      "void",
        "class",       "finally",     "long",        "strictfp",    "volatile",
        "const",       "float",       "native",      "super",       "while",

        // And distinguished names.
        "var", "record",
        // And literals
        "true", "false", "null"
    };


    // Static variables
    //////////////////////////////////

    private static List<String> KEYWORD_LIST;
    static {
        KEYWORD_LIST = Arrays.asList( JAVA_KEYWORDS );
    }

    // Instance variables
    //////////////////////////////////

    /** Schemagen options interface */
    protected SchemagenOptions m_options;

    /** The model that contains the input source */
    protected OntModel m_source;

    /** The output stream we write to */
    protected PrintStream m_output;

    /** The language used to retrieve comments */
    protected String m_lang;

    /** Option definitions */
    /** Stack of replacements to apply */
    protected List<Replacement> m_replacements = new ArrayList<>();

    /** Output file newline char - default is Unix, override with --dos */
    protected String m_nl = "\n";

    /** Size of indent step */
    protected int m_indentStep = 4;

    /** Set of names used so far */
    protected Set<String> m_usedNames = new HashSet<>();

    /** Map from resources to java names */
    protected Map<Resource,String> m_resourcesToNames = new HashMap<>();

    /** List of allowed namespace URI strings for admissible values */
    protected List<String> m_includeURI = new ArrayList<>();


    // Constructors
    //////////////////////////////////

    // External signature methods
    //////////////////////////////////

    /* Main entry point. See Javadoc for details of the many command line arguments */
    public static void main( String... args ) {
        try {
            new schemagen().go( args );
        }
        catch (SchemagenException e) {
            System.err.println( "Schemagen failed to run:" );
            System.err.println( e.getMessage() );

            if (e.getCause() != null) {
                System.err.println( "Caused by: " + e.getCause().getMessage() );
            }

            System.exit( 1 );
        }
    }


    // Internal implementation methods
    //////////////////////////////////

    /** Read the configuration parameters and do setup */
    protected void go( String[] args ) {
        go( new SchemagenOptionsImpl( args ) );
    }

    /** Handle initial configuration options, then initiate processing */
    protected void go( SchemagenOptions options ) {
        m_options = options;


        // check for user requesting help
        if (m_options.hasHelpOption()) {
            usage();
        }


        // got the configuration, now we can begin processing
        processInput();
    }

    /** The sequence of steps to process an entire file */
    protected void processInput() {
        addIncludes();
        determineLanguage();
        selectInput();
        selectLang();
        selectOutput();
        setGlobalReplacements();

        processHeader();
        writeClassDeclaration();
        writeInitialDeclarations();
        writeProperties();
        writeClasses();
        writeIndividuals();
        writeDatatypes();
        writeClassClose();
        processFooter();
        closeOutput();
    }

    /** Add the included files */
    protected void addIncludes() {
        // add any extra uri's that are allowed in the filter
        m_includeURI.addAll( m_options.getIncludeOption() );
    }

    /** Create the source model after determining which input language */
    protected void determineLanguage() {
        OntModelSpec s = null;
        if (m_options.hasLangRdfsOption()) {
            // RDFS language specified
            if (m_options.hasUseInfOption()) {
                s = OntModelSpec.RDFS_MEM_RDFS_INF;
            }
            else {
                s = OntModelSpec.RDFS_MEM;
            }
        }
        else {
            // owl is the default
            // s = OntModelSpec.getDefaultSpec( ProfileRegistry.OWL_LANG );
            if (m_options.hasUseInfOption()) {
                s = OntModelSpec.OWL_MEM_RULE_INF;
            }
            else {
                s = OntModelSpec.OWL_MEM;
            }
        }

        m_source = ModelFactory.createOntologyModel( s, null );
        m_source.getDocumentManager().setProcessImports( false );

        // turn off strict checking on request
        if (m_options.hasNoStrictOption()) {
            m_source.setStrictMode( false );
        }
    }

    /** Identify the URL that is to be read in and translated to a vocabulary file, and load the source into the source model */
    protected void selectInput() {
        if (!m_options.hasInputOption()) {
            usage();
        }

        String input = SchemagenUtils.urlCheck( m_options.getInputOption().getURI() );
        String syntax = m_options.getEncodingOption();

        try {
            m_source.read( input, syntax );
        }
        catch (JenaException e) {
            abort( "Failed to read input source " + input, e );
        }
    }

    protected void selectLang() {
        if (m_options.hasLangOption()) {
            m_lang = m_options.getLangOption();
        } else {
            m_lang = null;
        }
    }

    /** Identify the file we are to write the output to */
    protected void selectOutput() {
        String outFile = m_options.getOutputOption();
        if (outFile == null) {
            m_output = System.out;
        }
        else {
            try {
                // check for package name
                String packageName = m_options.getPackagenameOption();
                if (packageName != null) {
                    String packagePath = "";

                    // build the package path (e.g. com.foo.bar -> /com/foo/bar)
                    for (String p: packageName.split( "\\." )) {
                        packagePath = packagePath + File.separator + p;
                    }

                    if (!outFile.endsWith( packagePath )) {
                        outFile = outFile + packagePath;
                    }
                }

                File out = new File( outFile );

                if (!out.exists() && !outFile.endsWith( ".java" )) {
                    // create the directory if needed
                    out.mkdirs();
                }

                if (out.isDirectory()) {
                    // create a file in this directory named classname.java
                    String fileName = outFile + File.separator + getClassName() + ".java";
                    out = new File( fileName );
                }

                m_output = new PrintStream( new FileOutputStream( out ) );
            }
            catch (Exception e) {
                abort( "I/O error while trying to open file for writing: " + outFile, e );
            }
        }

        // check for DOS line endings
        if (m_options.hasDosOption()) {
            m_nl = "\r\n";
        }
    }

    /** Process the header at the start of the file, if defined */
    protected void processHeader() {
        String header = m_options.hasHeaderOption() ? m_options.getHeaderOption() : DEFAULT_HEADER_TEMPLATE;

        // user can turn of header processing, default is to have it on
        if (!m_options.hasNoheaderOption()) {
            writeln( 0, substitute( header ) );
        }
        else {
            // we have to do the imports at least
            writeln( 0, "import org.apache.jena.rdf.model.*;" );
            if (m_options.hasOntologyOption()) {
                writeln( 0, "import org.apache.jena.ontology.*;" );
            }
            if (m_options.hasIncludeSourceOption()) {
                writeln( 0, "import java.io.ByteArrayInputStream;" );
            }
        }
    }

    /** Process the footer at the end of the file, if defined */
    protected void processFooter() {
        String footer = m_options.getFooterOption();

        if (footer != null) {
            writeln( 0, substitute( footer ) );
        }
    }

    /** The list of replacements that are always available */
    protected void setGlobalReplacements() {
        addReplacementPattern( "date", new SimpleDateFormat( "dd MMM yyyy HH:mm").format( new Date() ) );
        addReplacementPattern( "package", m_options.hasPackagenameOption() ? ("package " + m_options.getPackagenameOption() + ";") : "" );
        addReplacementPattern( "imports", getImports() );
        addReplacementPattern( "classname", getClassName() );
        addReplacementPattern( "nl", m_nl );

        // protect \ in Windows file pathnames
        // looks for file:.* or C:.* (or variants thereof)
        String source = m_options.getInputOption().getURI();
        if (source.matches( "(file:|[A-Za-z]:).*$" )) {
            source = source.replace( "\\", "\\\\" );
        }
        addReplacementPattern( "sourceURI", source );
    }

    /** Add a pattern-value pair to the list of available patterns */
    protected void addReplacementPattern( String key, String replacement ) {
        if (replacement != null && key != null) {
            String marker = m_options.getMarkerOption();
            marker = (marker == null) ? DEFAULT_MARKER : marker;

            try {
                m_replacements.add( new Replacement( Pattern.compile( marker + key + marker ),
                                                     replacement ) );
            }
            catch (PatternSyntaxException e) {
                abort( "Malformed regexp pattern " + marker + key + marker, e );
            }
        }
    }

    /** Pop n replacements off the stack */
    protected void pop( int n ) {
        for (int i = 0;  i < n;  i++) {
            m_replacements.remove( m_replacements.size() - 1 );
        }
    }


    /** Close the output file */
    protected void closeOutput() {
        m_output.flush();
        m_output.close();
    }


    /** Abort due to exception */
    protected void abort( String msg, Exception cause ) {
        throw new SchemagenException( msg, cause );
    }

    /** Print usage message and abort */
    protected void usage() {
        System.err.println( "Usage:" );
        System.err.println( "  java jena.schemagen [options ...]" );
        System.err.println();
        System.err.println( "Commonly used options include:" );
        System.err.println( "   -i <input> the source document as a file or URL." );
        System.err.println( "   -l <lang> the desired language to use to retrieve comments." );
        System.err.println( "   -n <name> the name of the created Java class." );
        System.err.println( "   -a <uri> the namespace URI of the source document." );
        System.err.println( "   -o <file> the file to write the generated class into." );
        System.err.println( "   -o <dir> the directory in which the generated Java class is created." );
        System.err.println( "            By default, output goes to stdout." );
        System.err.println( "   -e <encoding> the encoding of the input document (N3, RDF/XML, etc)." );
        System.err.println( "   -c <config> a filename or URL for an RDF document containing " );
        System.err.println( "               configuration parameters." );
        System.err.println();
        System.err.println( "Many other options are available. See the schemagen HOWTO in the " );
        System.err.println( "Jena documentation for full details." );
        System.exit( 1 );
    }

    /** Use the current replacements list to do the subs in the given string */
    protected String substitute( String sIn ) {
        String s = sIn;

        for ( Replacement r : m_replacements )
        {
            s = r.pattern.matcher( s ).replaceAll( r.sub );
        }

        return s;
    }

    /** Add the appropriate indent to a buffer */
    protected int indentTo( int i, StringBuilder buf ) {
        int indent = i * m_indentStep;
        for (int j = 0;  j < indent; j++) {
            buf.append( ' ' );
        }

        return indent;
    }

    /** Write a blank line, with indent and newline */
    protected void writeln( int indent ) {
        writeln( indent, "" );
    }

    /** Write out the given string with n spaces of indent, with newline */
    protected void writeln( int indent, String s ) {
        write( indent, s );
        m_output.print( m_nl );
    }

    /** Write out the given string with n spaces of indent */
    protected void write( int indentLevel, String s ) {
        for (int i = 0;  i < (m_indentStep * indentLevel);  i++) {
            m_output.print( " " );
        }

        m_output.print( s );
    }

    /** Determine the list of imports to include in the file */
    protected String getImports() {
        StringBuilder buf = new StringBuilder();
        buf.append( "import org.apache.jena.rdf.model.*;" );
        buf.append( m_nl );

        if (useOntology()) {
            buf.append( "import org.apache.jena.ontology.*;" );
            buf.append( m_nl );
        }

        if (includeSource()) {
            buf.append( "import java.io.ByteArrayInputStream;" );
            buf.append( m_nl );
        }

        return buf.toString();
    }

    /** Determine the class name of the vocabulary from the URI */
    protected String getClassName() {
        // if a class name is given, just use that
        if (m_options.hasClassnameOption()) {
            return m_options.getClassnameOption();
        }

        // otherwise, we generate a name based on the URI
        String uri = m_options.getInputOption().getURI();

        // remove any suffixes
        uri = (uri.endsWith( "#" )) ? uri.substring( 0, uri.length() - 1 ) : uri;
        uri = (uri.endsWith( ".daml" )) ? uri.substring( 0, uri.length() - 5 ) : uri;
        uri = (uri.endsWith( ".owl" )) ? uri.substring( 0, uri.length() - 4 ) : uri;
        uri = (uri.endsWith( ".rdf" )) ? uri.substring( 0, uri.length() - 4 ) : uri;
        uri = (uri.endsWith( ".rdfs" )) ? uri.substring( 0, uri.length() - 5 ) : uri;
        uri = (uri.endsWith( ".n3" )) ? uri.substring( 0, uri.length() - 3 ) : uri;
        uri = (uri.endsWith( ".xml" )) ? uri.substring( 0, uri.length() - 4 ) : uri;
        uri = (uri.endsWith( ".ttl" )) ? uri.substring( 0, uri.length() - 4 ) : uri;

        // now work back to the first non name character from the end
        int i = uri.length() - 1;
        for (; i > 0; i--) {
            if (!Character.isUnicodeIdentifierPart( uri.charAt( i ) ) &&
                uri.charAt( i ) != '-') {
                i++;
                break;
            }
        }

        String name = uri.substring( i );

        // optionally add name suffix
        if (m_options.hasClassnameSuffixOption()) {
            name = name + m_options.getClassnameSuffixOption();
        }

        // now we make the name into a legal Java identifier
        return asLegalJavaID( name, true );
    }

    /** Answer true if we are using ontology terms in this vocabulary */
    protected boolean useOntology() {
        return m_options.hasOntologyOption();
    }

    /** Answer true if all comments are suppressed */
    protected boolean noComments() {
        return m_options.hasNoCommentsOption();
    }

    /** Answer true if ontology source code is to be included */
    protected boolean includeSource() {
        return m_options.hasIncludeSourceOption();
    }

    /** Converts to a legal Java identifier; capitalise first char if cap is true */
    protected String asLegalJavaID( String s, boolean cap ) {
        StringBuilder buf = new StringBuilder();
        int i = 0;

        // treat the first character specially - must be able to start a Java ID, may have to up-case
        try {
            for (; !Character.isJavaIdentifierStart( s.charAt( i )); i++) { /**/ }
        }
        catch (StringIndexOutOfBoundsException e) {
            System.err.println( "Could not identify legal Java identifier start character in '" + s + "', replacing with __" );
            return "__";
        }
        buf.append( cap ? Character.toUpperCase( s.charAt( i ) ) : s.charAt( i ) );

        // copy the remaining characters - replace non-legal chars with '_'
        for (++i; i < s.length(); i++) {
            char c = s.charAt( i );
            buf.append( Character.isJavaIdentifierPart( c ) ? c : '_' );
        }

        // check for illegal keyword
        if (KEYWORD_LIST.contains( buf.toString() )) {
            buf.append( '_' );
        }

        return buf.toString();
    }

    /** The opening class declaration */
    protected void writeClassDeclaration() {
        writeln( 0, "@SuppressWarnings({\"removal\", \"deprecated\"})");
        write( 0, "public class " );
        write( 0, getClassName() );
        write( 0, " " );

        if (m_options.hasClassdecOption()) {
            write( 0, m_options.getClassdecOption() );
        }

        writeln( 0, "{" );
    }

    /** The close of the class decoration */
    protected void writeClassClose() {
        writeln( 0, "}" );
    }

    /** Write the declarations at the head of the class */
    protected void writeInitialDeclarations() {
        writeModelDeclaration();
        writeSource();
        writeNamespace();
        writeOntologyVersionInfo();

        if (m_options.hasDeclarationsOption()) {
            writeln( 0, m_options.getDeclarationsOption() );
        }
    }

    /** Write the declaration of the model */
    protected void writeModelDeclaration() {
        if (useOntology()) {
            String lang = "OWL";
            if (m_options.hasLangRdfsOption()) {
                lang = "RDFS";
            }
            writeln( 1, "/** <p>The ontology model that holds the vocabulary terms</p> */" );
            writeln( 1, "private static final OntModel M_MODEL = ModelFactory.createOntologyModel( OntModelSpec." + lang + "_MEM, null );" );
        }
        else {
            writeln( 1, "/** <p>The RDF model that holds the vocabulary terms</p> */" );
            writeln( 1, "private static final Model M_MODEL = ModelFactory.createDefaultModel();" );
        }

        writeln( 1 );
    }

    /** Write the source code of the input model into the file itself */
    protected void writeSource() {
        if (includeSource()) {
            String output = RDFWriter.source(m_source).format(RDFFormat.TURTLE_BLOCKS).asString();

            // now we embed each line of the source in the output
            writeln( 1, "private static final String SOURCE = " );
            boolean first = true;

            StringTokenizer st = new StringTokenizer( output, "\n" );
            while (st.hasMoreTokens()) {
                String tok = st.nextToken();
                if (tok.endsWith( "\r" )) {
                    tok = tok.substring( 0, tok.length() - 1 );
                }
                write( 2, first ? "   " : " + " );
                write( 0, "\"" );
                write( 0, protectQuotes( tok ) );
                writeln( 2, "\\n\"" );
                first = false;
            }

            // then we reference the string constant when reading the source
            // note that we avoid StringReader due to charset encoding issues
            writeln( 1, ";" );
            writeln( 0, "" );
            writeln( 1, "/** Read the ontology definition into the source model */ " );
            writeln( 1, "static { " );
            writeln( 2, "M_MODEL.read( new ByteArrayInputStream( SOURCE.getBytes() ), null, \"N3\" );" );
            writeln( 1, "}" );
            writeln( 0, "" );
        }
    }

    /** Protect any double quotes in the given string so that it's a legal Java String */
    private String protectQuotes( String s ) {
        return s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\"", "\\\\\"" );
    }

    /** Write owl:versionInfo string if it exists **/
    protected void writeOntologyVersionInfo() {
        String versionInfo = getOntologyElementVersionInfo();

        if (null != versionInfo) {
            writeln( 1, "/** <p>The ontology's owl:versionInfo as a string</p> */" );
            writeln( 1, "public static final String VERSION_INFO = \"" + protectQuotes(versionInfo) + "\";" );
            writeln( 1 );
        }
    }

    /** Write the string and resource that represent the namespace */
    protected void writeNamespace() {
        String nsURI = determineNamespaceURI();

        writeln( 1, "/** <p>The namespace of the vocabulary as a string</p> */" );
        writeln( 1, "public static final String NS = \"" + nsURI + "\";" );
        writeln( 1 );

        writeln( 1, "/** <p>The namespace of the vocabulary as a string</p>" );
        writeln( 1, " * @return namespace as String" );
        writeln( 1, " * @see #NS */" );
        writeln( 1, "public static String getURI() {return NS;}" );
        writeln( 1 );

        writeln( 1, "/** <p>The namespace of the vocabulary as a resource</p> */" );
        writeln( 1, "public static final Resource NAMESPACE = M_MODEL.createResource( NS );" );
        writeln( 1 );
    }


    /** Determine what the namespace URI for this vocabulary is */
    protected String determineNamespaceURI() {
        // we have a sequence of strategies for determining the ontology namespace
        String ns = getOptionNamespace();
        if (ns == null) {
            ns = getDefaultPrefixNamespace();
        }
        if (ns == null) {
            ns = getOntologyElementNamespace();
        }
        if (ns == null) {
            ns = guessNamespace();
        }

        // did we get one?
        if (ns == null) {
            abort( "Could not determine the base URI for the input vocabulary", null );
        }

        m_includeURI.add( ns );
        return ns;
    }

    /** User has set namespace via a schemagen option */
    protected String getOptionNamespace() {
        return m_options.hasNamespaceOption() ? m_options.getNamespaceOption().getURI() : null;
    }

    /** Document has set an empty prefix for the model */
    protected String getDefaultPrefixNamespace() {
        // alternatively, the default namespace may be set in the prefix mapping read from the input document
        String defaultNS = m_source.getNsPrefixURI( "" );
        if (defaultNS == null) {
            defaultNS = m_source.getBaseModel().getNsPrefixURI( "" );
        }

        return defaultNS;
    }

    /** Document has an owl:Ontology or daml:Ontology element */
    protected String getOntologyElementVersionInfo() {
        String versionInfo = null;

        Resource ontologyClass = m_source.getProfile().ONTOLOGY();
        if (null != ontologyClass) {
            StmtIterator i = m_source.getBaseModel().listStatements( null, RDF.type, ontologyClass );
            if (i.hasNext()) {
                Resource ont = i.nextStatement().getSubject();
                StmtIterator j = m_source.getBaseModel().listStatements( ont, OWL.versionInfo, (RDFNode)null );
                if (j.hasNext()) {
                    RDFNode versionInfoObject = j.nextStatement().getObject();
                    if (versionInfoObject.isResource())
                        versionInfo = versionInfoObject.asResource().getURI();
                    else
                        versionInfo = versionInfoObject.asLiteral().getLexicalForm();

                    // check for ambiguous answers
                    if (j.hasNext()) {
                        System.err.println( "Warning: ambiguous owl:versionInfo - there are more than one owl:versionInfo statements." );
                        System.err.println( "Picking first choice: " + versionInfo  + ". Other choices are:" );
                        while (j.hasNext()) {
                            System.err.print( " " );
                            System.err.print( j.nextStatement().getObject().toString() );
                        }
                        System.err.println();
                    }
                }

                // check for ambiguous answers
                if (i.hasNext()) {
                    System.err.println( "Warning: ambiguous owl:versionInfo - there is more than one owl:Ontology element." );
                    System.err.println( "Picking first choice: " + ont.getURI()  + ". Other choices are:" );
                    while (i.hasNext()) {
                        System.err.print( " " );
                        System.err.print( i.nextStatement().getObject().toString() );
                    }
                    System.err.println();
                }
            }
        }

        return versionInfo;
    }

    /** Document has an owl:Ontology or daml:Ontology element */
    protected String getOntologyElementNamespace() {
        // if we are using an ontology model, we can get the namespace URI from the ontology element
        String uri = null;

        StmtIterator i = m_source.getBaseModel()
                                 .listStatements( null, RDF.type, m_source.getProfile().ONTOLOGY() );

        if (i.hasNext()) {
            Resource ont = i.nextStatement().getSubject();
            uri = ont.getURI();

            // ensure ends with namespace separator char
            char ch = uri.charAt( uri.length() - 1 );
            boolean endsWithNCNameCh = XML10Char.isNCName( ch );
            uri = endsWithNCNameCh ? uri + "#" : uri;

            // check for ambiguous answers
            if (i.hasNext()) {
                System.err.println( "Warning: ambiguous default namespace - there is more than one owl:Ontology element." );
                System.err.println( "Picking first choice: " + uri  + ". Other choices are:" );
                while (i.hasNext()) {
                    System.err.print( " " );
                    System.err.print( i.nextStatement().getString() );
                }
                System.err.println();
                System.err.println( "Use the -a option to specify a particular namespace if required." );
            }
        }

        return uri;
    }

    /** Guess the URI from the most prevalent URI */
    protected String guessNamespace() {
        Map<String,Integer> nsCount = new HashMap<>();

        // count all of the namespaces used in the model
        for (StmtIterator i = m_source.listStatements(); i.hasNext(); ) {
            Statement s = i.next();
            countNamespace( s.getSubject(), nsCount );
            countNamespace( s.getPredicate(), nsCount );
            if (s.getObject().isResource()) {
                countNamespace( s.getResource(), nsCount );
            }
        }

        // now find the maximal element
        String ns = null;
        int max = 0;
        for ( String nsKey : nsCount.keySet() )
        {
            // we ignore the usual suspects
            if ( !( OWL.getURI().equals( nsKey ) ||
                RDF.getURI().equals( nsKey ) ||
                RDFS.getURI().equals( nsKey ) ||
                XSD.getURI().equals( nsKey ) ) )
            {
                // not an ignorable namespace
                int count = nsCount.get( nsKey ).intValue();

                if ( count > max )
                {
                    // highest count seen so far
                    max = count;
                    ns = nsKey;
                }
            }
        }

        return ns;
    }

    /** Record a use of the given namespace in the count map */
    private void countNamespace( Resource r, Map<String,Integer> nsCount ) {
        if (!r.isAnon()) {
            String ns = r.getNameSpace();

            // increment the count for this namespace
            Integer count = nsCount.containsKey( ns ) ? (Integer) nsCount.get( ns ) : Integer.valueOf( 0 );
            Integer count1 = Integer.valueOf( count.intValue() + 1 );

            nsCount.put( ns, count1 );
        }
    }

    /** Write the list of properties */
    protected void writeProperties() {
        if (m_options.hasNopropertiesOption()) {
            return;
        }

        if (m_options.hasPropertySectionOption()) {
            writeln( 0, m_options.getPropertySectionOption());
        }

        if (useOntology()) {
            writeObjectProperties();
            writeDatatypeProperties();
            writeAnnotationProperties();

            // we also write out the RDF properties, to mop up any props that are not stated as
            // object, datatype or annotation properties
            writeRDFProperties( true );
        }
        else {
            writeRDFProperties( false );
        }
    }

    /** Write any object properties in the vocabulary */
    protected void writeObjectProperties() {
        String template = m_options.hasPropTemplateOption() ?  m_options.getPropTemplateOption() : DEFAULT_TEMPLATE;

        if (!m_options.hasLangRdfsOption()) {
            for (Iterator<? extends RDFNode> i = sorted( m_source.listObjectProperties() ); i.hasNext(); ) {
                writeValue( (Resource) i.next(), template, "ObjectProperty", "createObjectProperty", "_PROP" );
            }
        }
    }

    /** Write any datatype properties in the vocabulary */
    protected void writeDatatypeProperties() {
        String template = m_options.hasPropTemplateOption() ?  m_options.getPropTemplateOption() : DEFAULT_TEMPLATE;

        if (!m_options.hasLangRdfsOption()) {
            for (Iterator<? extends RDFNode> i = sorted( m_source.listDatatypeProperties() ); i.hasNext(); ) {
                writeValue( (Resource) i.next(), template, "DatatypeProperty", "createDatatypeProperty", "_PROP" );
            }
        }
    }

    /** Write any annotation properties in the vocabulary */
    protected void writeAnnotationProperties() {
        String template = m_options.hasPropTemplateOption() ?  m_options.getPropTemplateOption() : DEFAULT_TEMPLATE;

        if (!m_options.hasLangRdfsOption()) {
            for (Iterator<? extends RDFNode> i = sorted( m_source.listAnnotationProperties() ); i.hasNext(); ) {
                writeValue( (Resource) i.next(), template, "AnnotationProperty", "createAnnotationProperty", "_PROP" );
            }
        }
    }

    /** Write any vanilla RDF properties in the vocabulary */
    protected void writeRDFProperties( boolean useOntProperty ) {
        String template = m_options.hasPropTemplateOption() ?  m_options.getPropTemplateOption() : DEFAULT_TEMPLATE;
        String propType = useOntProperty ? "OntProperty" : "Property";

        // select the appropriate properties based on the language choice
        Resource[] props;
        if (m_options.hasLangOwlOption()) {
            props = new Resource[] {OWL.ObjectProperty, OWL.DatatypeProperty, RDF.Property};
        }
        else {
            props = new Resource[] {RDF.Property};
        }

        // collect the properties to be written
        List<Resource> propertyResources = new ArrayList<>();
        for ( Resource prop : props )
        {
            for ( StmtIterator i = m_source.listStatements( null, RDF.type, prop ); i.hasNext(); )
            {
                propertyResources.add( i.nextStatement().getSubject() );
            }
        }

        // now write the properties
        for (Iterator<? extends RDFNode> i = sorted( propertyResources ); i.hasNext(); ) {
            writeValue( (Resource) i.next(), template, propType, "create" + propType, "_PROP" );
        }
    }

    /** Write any classes in the vocabulary */
    protected void writeClasses() {
        if (m_options.hasNoclassesOption()) {
            return;
        }

        if (m_options.hasClassSectionOption()) {
            writeln( 0, m_options.getClassSectionOption());
        }

        if (useOntology()) {
            writeOntClasses();
        }
        else {
            writeRDFClasses();
        }
    }

    /** Write classes as ontology terms */
    protected void writeOntClasses() {
        String template = m_options.hasClassTemplateOption() ?  m_options.getClassTemplateOption() : DEFAULT_TEMPLATE;

        for (Iterator<? extends RDFNode> i = sorted( m_source.listClasses() ); i.hasNext(); ) {
            writeValue( (Resource) i.next(), template, "OntClass", "createClass", "_CLASS" );
        }
    }

    /** Write classes as vanilla RDF terms */
    protected void writeRDFClasses() {
        String template = m_options.hasClassTemplateOption() ?  m_options.getClassTemplateOption() : DEFAULT_TEMPLATE;

        // make sure we're looking for the appropriate type of class
        Resource cls = OWL.Class;
        if (m_options.hasLangRdfsOption()) {
            cls = RDFS.Class;
        }

        // collect the classes to list
        List<Resource> classes = m_source.listStatements( null, RDF.type, cls ).mapWith( s -> s.getSubject()).toList();

        for (Iterator<? extends RDFNode> i = sorted( classes ); i.hasNext(); ) {
            writeValue( (Resource) i.next(), template, "Resource", "createResource", "_CLASS" );
        }
    }

    /** Write any instances (individuals) in the vocabulary */
    protected void writeIndividuals() {
        if (m_options.hasNoindividualsOption()) {
            return;
        }

        if (m_options.hasIndividualsSectionOption()) {
            writeln( 0, m_options.getIndividualsSectionOption() );
        }

        if (useOntology()) {
            writeOntIndividuals();
        }
        else {
            writeRDFIndividuals();
        }
    }

    /** Write individuals as ontology terms */
    protected void writeOntIndividuals() {
        String template = m_options.hasIndividualTemplateOption() ?  m_options.getIndividualTemplateOption() : DEFAULT_INDIVIDUAL_TEMPLATE;

        for (Iterator<? extends RDFNode> i = selectIndividuals(); i.hasNext(); ) {
            Individual ind = i.next().as( Individual.class );

            // do we have a local class resource
            Resource cls = ind.getOntClass();
            if (cls == null) { cls = OWL.Thing; }

            String varName = m_resourcesToNames.get( cls );
            String valType = (varName != null) ? varName : "M_MODEL.createClass( \"" + cls.getURI() + "\" )";

            // push the individuals type onto the stack
            addReplacementPattern( "valtype", valType );
            writeValue( ind, template, "Individual", "createIndividual", "_INSTANCE" );
            pop( 1 );

        }
    }

    /** Write individuals as vanilla RDF terms */
    protected void writeRDFIndividuals() {
        String template = m_options.hasIndividualTemplateOption() ?  m_options.getIndividualTemplateOption() : DEFAULT_RDFS_INDIVIDUAL_TEMPLATE;

        for (Iterator<? extends RDFNode> i = selectIndividuals(); i.hasNext(); ) {
            writeValue( (Resource) i.next(), template, "Resource", "createResource", "_INSTANCE" );
        }
    }

    /** Answer an iterator over the individuals selected for output */
    protected ExtendedIterator<? extends RDFNode> selectIndividuals() {
        List<Resource> candidates = new ArrayList<>();
        for (StmtIterator i = m_source.listStatements( null, RDF.type, (RDFNode) null ); i.hasNext(); ) {
            Statement candidate = i.nextStatement();

            if (candidate.getObject().isResource()) {
                Resource candObj = candidate.getResource();
                Resource candSubj = candidate.getSubject();

                // ignore RDFS and OWL builtins
                if (!candObj.isAnon()) {
                    String candTypeURI = candObj.getURI();
                    if (candTypeURI.startsWith( RDF.getURI() ) ||
                        candTypeURI.startsWith( OWL.getURI() ) ||
                        candTypeURI.startsWith( RDFS.getURI() )) {
                        continue;
                    }
                }

                // note that whether candSubj is included is tested later on by {@link #filter}
                if (!candSubj.isAnon() && (isIncluded( candObj ) || isIncluded( candSubj )) && !candidates.contains( candSubj )) {
                    candidates.add( candSubj );
                }
            }
        }

        return sorted( candidates );
    }

    /**
     * Answer true if the given resource is accepted for presentation in the output, which
     * is true iff it is a URI node, whose namespace is one of the accepted namespaces in
     * {@link #m_includeURI}.
     * @param r A resource to test
     * @return True if the resource is to be included in the generated output
     */
    protected boolean isIncluded( Resource r ) {
        boolean accepted = false;

        if (!r.isAnon()) {
            String uri = r.getURI();
            for (Iterator<String> j = m_includeURI.iterator();  !accepted && j.hasNext(); ) {
                accepted = uri.startsWith( j.next() );
            }
        }

        return accepted;
    }

    /** Write any datatypes in the vocabulary */
    protected void writeDatatypes() {
        if (m_options.hasNodatatypesOption()) {
            return;
        }

        if (m_options.hasDatatypesSectionOption()) {
            writeln( 0, m_options.getDatatypesSectionOption() );
        }

        String template = m_options.hasDatatypeTemplateOption() ?  m_options.getDatatypeTemplateOption() : DEFAULT_TEMPLATE;

        // Cannot create a full RDFDatatype object since we don't know how to parse these custom types, but we can at least specify a Resource
        for (Iterator<? extends RDFNode> i = selectDatatypes(); i.hasNext(); ) {
            writeValue( (Resource) i.next(), template, "Resource", "createResource", "_DATATYPE" );
        }
    }

    /** Answer an iterator over the datatypes selected for output */
    protected ExtendedIterator<? extends RDFNode> selectDatatypes() {
        List<Resource> candidates = new ArrayList<>();
        for (StmtIterator i = m_source.listStatements( null, RDF.type, RDFS.Datatype ); i.hasNext(); ) {
            Statement candidate = i.nextStatement();

            if (candidate.getObject().isResource()) {
                Resource candSubj = candidate.getSubject();

                // ignore XSD builtins
                if (!candSubj.isAnon()) {
                    String candTypeURI = candSubj.getURI();
                    if (candTypeURI.startsWith( XSD.getURI() )) {
                        continue;
                    }
                }

                // note that whether candSubj is included is tested later on by {@link #filter}
                if (!candSubj.isAnon() && !candidates.contains( candSubj )) {
                    candidates.add( candSubj );
                }
            }
        }

        return sorted( candidates );
    }

    /** Write the value declaration out using the given template, optionally creating comments */
    protected void writeValue( Resource r, String template, String valueClass, String creator, String disambiguator ) {
        if (!filter( r )) {
            if (!noComments()  &&  hasComment( r )) {
                writeln( 1, formatComment( getComment( r ) ) );
            }

            // push the local bindings for the substitution onto the stack
            addReplacementPattern( "valuri", r.getURI() );
            addReplacementPattern( "valname", getValueName( r, disambiguator ));
            addReplacementPattern( "valclass", valueClass );
            addReplacementPattern( "valcreator", creator );

            // write out the value
            writeln( 1, substitute( template ) );
            writeln( 1 );

            // pop the local replacements off the stack
            pop( 4 );
        }
    }

    /** Answer true if the given resource has an rdf:comment */
    protected boolean hasComment( Resource r ) {
        return r.hasProperty( RDFS.comment ) ;
    }

    /** Answer all of the commentary on the given resource, as a string */
    protected String getComment( Resource r ) {
        StringBuilder comment = new StringBuilder();

        // collect any RDFS or DAML comments attached to the node
        for (NodeIterator ni = m_source.listObjectsOfProperty( r, RDFS.comment );  ni.hasNext(); ) {
            RDFNode n = ni.nextNode();
            if (n instanceof Literal) {
                if (m_lang == null || m_lang.trim().equals("")) {
                    // default behavior, where no language was specified
                    comment.append( ((Literal) n).getLexicalForm().trim() );
                } else if (((Literal) n).getLanguage().equals(m_lang)) {
                    // otherwise only use comment that matches the language given
                    comment.append( ((Literal) n).getLexicalForm().trim() );
                }
            }
            else {
                System.err.println( "Warning: Comment on resource <" + r.getURI() + "> is not a literal: " + n );
            }
        }

        return comment.toString();
    }

    /** Format the comment as Javadoc, and limit the line width */
    protected String formatComment( String comment ) {
        StringBuilder buf = new StringBuilder();
        buf.append( "/** <p>" );

        boolean inSpace = false;
        int pos = buf.length();
        boolean singleLine = true;

        // now format the comment by compacting whitespace and limiting the line length
        // add the prefix to the start of each line
        for (int i = 0;  i < comment.length();  i++ ) {
            char c = comment.charAt( i );

            // compress whitespace
            if (Character.isWhitespace( c )) {
                if (inSpace) {
                    continue;       // more than one space is ignored
                }
                else {
                    c = ' ';        // map all whitespace to 0x20
                    inSpace = true;
                }
            }
            else {
                inSpace = false;
            }

            // escapes?
            if (c == '\\') {
                c = comment.charAt( ++i );

                switch (c) {
                    case 'n':
                        buf.append( m_nl );
                        pos = indentTo( 1, buf );
                        buf.append( " *  " );
                        pos += 3;
                        singleLine = false;
                        break;

                    default:
                        // add other escape sequences above
                        break;
                }
            }
            else if (c == '<') {
                buf.append( "&lt;" );
                pos += 4;
            }
            else if (c == '>') {
                buf.append( "&gt;" );
                pos += 4;
            }
            else if (c == '&') {
                buf.append( "&amp;" );
                pos += 5;
            }
            else {
                // add the char
                buf.append( c );
                pos++;
            }

            // wrap any very long lines at 120 chars
            if ((pos > COMMENT_LENGTH_LIMIT) && (inSpace)) {
                buf.append( m_nl );
                pos = indentTo( 1, buf );
                buf.append( " *  " );
                pos += 3;
                singleLine = false;
            }
        }

        buf.append( "</p>" );
        buf.append( singleLine ? "" : m_nl );
        indentTo( singleLine ? 0 : 1, buf );
        buf.append( " */" );
        return buf.toString();
    }

    /** Answer true if resource r <b>does not</b> show in output */
    protected boolean filter( Resource r ) {
        if (r.isAnon()) {
            return true;
        }

        // if we've already processed this resource once, ignore it next time
        if (m_resourcesToNames.containsKey( r )) {
            return true;
        }

        // search the allowed URI's
        for ( String uri : m_includeURI )
        {
            if ( r.getURI().startsWith( uri ) )
            {
                // in
                return false;
            }
        }

        // we allow individuals whose class is not in the included NS's, unless opt strict-individuals is true */
        if (!m_options.hasStrictIndividualsOption()) {
            for (StmtIterator j = r.listProperties( RDF.type ); j.hasNext(); ) {
                // we search the rdf:types of this resource
                Resource typeRes = j.nextStatement().getResource();

                if (!typeRes.isAnon()) {
                    String typeURI = typeRes.getURI();

                    // for any type that is in a permitted NS
                    for ( String uri : m_includeURI )
                    {
                        if ( typeURI.startsWith( uri ) )
                        {
                            // in
                            return false;
                        }
                    }
                }
            }
        }

        // default is out
        return true;
    }

    /** Answer the Java value name for the URI */
    protected String getValueName( Resource r, String disambiguator ) {
        // the id name is basically the local name of the resource, possibly in upper case
        String name = m_options.hasUcNamesOption() ? getUCValueName( r ) : r.getLocalName();

        // must be legal java
        name = asLegalJavaID( name, false );

        // must not clash with an existing name
        int attempt = 0;
        String baseName = name;
        while (m_usedNames.contains( name )) {
            name = (attempt == 0) ? (name + disambiguator) : (baseName + disambiguator + attempt);
            attempt++;
        }

        // record this name so that we don't use it again (which will stop the vocabulary from compiling)
        m_usedNames.add( name );

        // record the mapping from resource to name
        m_resourcesToNames.put( r, name );

        return name;
    }

    /** Answer the local name of resource r mapped to upper case */
    protected String getUCValueName( Resource r ) {
        StringBuilder buf = new StringBuilder();
        String localName = r.getLocalName();
        char lastChar = 0;

        for (int i = 0; i < localName.length(); i++) {
            char c = localName.charAt(i);

            if (Character.isLowerCase(lastChar) && Character.isUpperCase(c)) {
                buf.append( '_' );
            }
            buf.append( Character.toUpperCase(c) );
            lastChar = c;
        }

        return buf.toString();
    }

    /** Answer an iterator that contains the elements of the given list, but sorted by URI */
    protected ExtendedIterator<? extends RDFNode> sorted( ExtendedIterator<? extends RDFNode> i ) {
        return sorted( i.toList() );
    }

    /** Answer an iterator that contains the elements of the given iterator, but sorted by URI */
    protected ExtendedIterator<? extends RDFNode> sorted( List<? extends RDFNode> members ) {
        Collections.sort( members, new Comparator<RDFNode>() {
            @Override
            public int compare( RDFNode n0, RDFNode n1 ) {
                if (n0.isLiteral() || n1.isLiteral()) {
                    if (n0.isLiteral() && n1.isLiteral()) {
                        // two literals
                        Literal l0 = (Literal) n0;
                        Literal l1 = (Literal) n1;
                        return l0.getLexicalForm().compareTo( l1.getLexicalForm() );
                    }
                    else {
                        return n0.isLiteral() ? -1 : 1;
                    }
                }
                else {
                    Resource r0 = (Resource) n0;
                    Resource r1 = (Resource) n1;
                    if (r0.isAnon() && r1.isAnon()) {
                        // two anonID's - the order is important as long as its consistent
                        return r0.getId().toString().compareTo( r1.getId().toString() );
                    }
                    else if (r0.isAnon()) {
                        return -1;
                    }
                    else if (r1.isAnon()) {
                        return 1;
                    }
                    else {
                        // two named resources
                        return r0.getURI().compareTo( r1.getURI() );
                    }
                }
            }} );

        return WrappedIterator.create( members.iterator() );
    }

    //==============================================================================
    // Inner class definitions
    //==============================================================================

    public interface SchemagenOptions {
        /* Constants for the various options we can set */

        public enum OPT {
            /** Select an alternative config file; use <code>-c &lt;filename&gt;</code> on command line */
            CONFIG_FILE,

            /** Turn off all comment output; use <code>--nocomments</code> on command line;  use <code>sgen:noComments</code> in config file */
            NO_COMMENTS,

            /** Nominate the URL of the input document; use <code>-i &lt;URL&gt;</code> on command line;  use <code>sgen:input</code> in config file */
            INPUT,

            /** Specify the language used to retrieve comments */
            LANG,

            /** Specify that the language of the source is DAML+OIL; use <code>--daml</code> on command line;  use <code>sgen:daml</code> in config file */
            LANG_DAML,

            /** Specify that the language of the source is OWL (the default); use <code>--owl</code> on command line;  use <code>sgen:owl</code> in config file */
            LANG_OWL,

            /** Specify that the language of the source is RDFS; use <code>--rdfs</code> on command line;  use <code>sgen:rdfs</code> in config file */
            LANG_RDFS,

            /** Specify that destination file; use <code>-o &lt;fileName&gt;</code> on command line;  use <code>sgen:output</code> in config file */
            OUTPUT,

            /** Specify the file header; use <code>--header "..."</code> on command line;  use <code>sgen:header</code> in config file */
            HEADER,

            /** Specify the file footer; use <code>--footer "..."</code> on command line;  use <code>sgen:footer</code> in config file */
            FOOTER,

            /** Specify the uri of the configuration root node; use <code>--root &lt;URL&gt;</code> on command line */
            ROOT,

            /** Specify the marker string for substitutions, default is '%'; use <code>-m "..."</code> on command line; use <code>sgen:marker</code> in config file */
            MARKER,

            /** Specify the packagename; use <code>--package &lt;packagename&gt;</code> on command line; use <code>sgen:package</code> in config file */
            PACKAGENAME,

            /** Use ontology terms in preference to vanilla RDF; use <code>--ontology</code> on command line; use <code>sgen:ontology</code> in config file */
            ONTOLOGY,

            /** The name of the generated class; use <code>-n &lt;classname&gt;</code> on command line; use <code>sgen:classname</code> in config file */
            CLASSNAME,

            /** Additional decoration for class header (such as implements); use <code>--classdec &lt;classname&gt;</code> on command line; use <code>sgen:classdec</code> in config file */
            CLASSDEC,

            /** The namespace URI for the vocabulary; use <code>-a &lt;uri&gt;</code> on command line; use <code>sgen:namespace</code> in config file */
            NAMESPACE,

            /** Additional declarations to add at the top of the class; use <code>--declarations &lt;...&gt;</code> on command line; use <code>sgen:declarations</code> in config file */
            DECLARATIONS,

            /** Section declaration for properties section; use <code>--propSection &lt;...&gt;</code> on command line; use <code>sgen:propSection</code> in config file */
            PROPERTY_SECTION,

            /** Section declaration for class section; use <code>--classSection &lt;...&gt;</code> on command line; use <code>sgen:classSection</code> in config file */
            CLASS_SECTION,

            /** Section declaration for individuals section; use <code>--individualsSection &lt;...&gt;</code> on command line; use <code>sgen:individualsSection</code> in config file */
            INDIVIDUALS_SECTION,

            /** Section declaration for datatypes section; use <code>--datatypesSection &lt;...&gt;</code> on command line; use <code>sgen:datatypesSection</code> in config file */
            DATATYPES_SECTION,

            /** Option to suppress properties in vocab file; use <code>--noproperties &lt;...&gt;</code> on command line; use <code>sgen:noproperties</code> in config file */
            NOPROPERTIES,

            /** Option to suppress classes in vocab file; use <code>--noclasses &lt;...&gt;</code> on command line; use <code>sgen:noclasses</code> in config file */
            NOCLASSES,

            /** Option to suppress individuals in vocab file; use <code>--noindividuals &lt;...&gt;</code> on command line; use <code>sgen:noindividuals</code> in config file */
            NOINDIVIDUALS,

            /** Option to suppress datatypes in vocab file; use <code>--nodatatypes &lt;...&gt;</code> on command line; use <code>sgen:nodatatypes</code> in config file */
            NODATATYPES,

            /** Option for no file header; use <code>--noheader &lt;...&gt;</code> on command line; use <code>sgen:noheader</code> in config file */
            NOHEADER,

            /** Template for writing out property declarations; use <code>--propTemplate &lt;...&gt;</code> on command line; use <code>sgen:propTemplate</code> in config file */
            PROP_TEMPLATE,

            /** Template for writing out class declarations; use <code>--classTemplate &lt;...&gt;</code> on command line; use <code>sgen:classTemplate</code> in config file */
            CLASS_TEMPLATE,

            /** Template for writing out individual declarations; use <code>--individualTemplate &lt;...&gt;</code> on command line; use <code>sgen:individualTemplate</code> in config file */
            INDIVIDUAL_TEMPLATE,

            /** Template for writing out datatype declarations; use <code>--datatypeTemplate &lt;...&gt;</code> on command line; use <code>sgen:datatypeTemplate</code> in config file */
            DATATYPE_TEMPLATE,

            /** Option for mapping constant names to uppercase; use <code>--uppercase &lt;...&gt;</code> on command line; use <code>sgen:uppercase</code> in config file */
            UC_NAMES,

            /** Option for including non-local URI's in vocabulary; use <code>--include &lt;uri&gt;</code> on command line; use <code>sgen:include</code> in config file */
            INCLUDE,

            /** Option for adding a suffix to the generated class name; use <code>--classnamesuffix &lt;uri&gt;</code> on command line; use <code>sgen:classnamesuffix</code> in config file */
            CLASSNAME_SUFFIX,

            /** Option for the presentation syntax (encoding) of the file; use <code>-e <i>encoding</i></code> on command line; use <code>sgen:encoding</code> in config file */
            ENCODING,

            /** Option to show the usage message; use --help on command line */
            HELP,

            /** Option to generate an output file with DOS (\r\n) line endings. Default is Unix line endings. */
            DOS,

            /** Option to generate to force the model to perform inference, off by default. */
            USE_INF,

            /** Option to exclude instances of classes in the allowed namespaces, where the individuals themselves are in other namespaces; use <code>--strictIndividuals</code> on command line; use <code>sgen:strictIndividuals</code> in config file */
            STRICT_INDIVIDUALS,

            /** Option to include the ontology source code in the generated file */
            INCLUDE_SOURCE,

            /** Option to turn off strict checking in .a() */
            NO_STRICT
        }


        public static final Object[][] m_optionDefinitions = new Object[][] {
                {OPT.CONFIG_FILE,         new OptionDefinition( "-c", "configFile" ) },
                {OPT.ROOT,                new OptionDefinition( "-r", "root" ) },
                {OPT.NO_COMMENTS,         new OptionDefinition( "--nocomments", "noComments" ) },
                {OPT.INPUT,               new OptionDefinition( "-i", "input" ) },
                {OPT.LANG,                new OptionDefinition( "-l", "lang" ) },
                {OPT.LANG_DAML,           new OptionDefinition( "--daml", "daml" ) },
                {OPT.LANG_OWL,            new OptionDefinition( "--owl", "owl" ) },
                {OPT.LANG_RDFS,           new OptionDefinition( "--rdfs", "rdfs" ) },
                {OPT.OUTPUT,              new OptionDefinition( "-o", "output" ) },
                {OPT.HEADER,              new OptionDefinition( "--header", "header" ) },
                {OPT.FOOTER,              new OptionDefinition( "--footer", "footer" ) },
                {OPT.MARKER,              new OptionDefinition( "--marker", "marker" ) },
                {OPT.PACKAGENAME,         new OptionDefinition( "--package", "package" ) },
                {OPT.ONTOLOGY,            new OptionDefinition( "--ontology", "ontology" ) },
                {OPT.CLASSNAME,           new OptionDefinition( "-n", "classname" ) },
                {OPT.CLASSDEC,            new OptionDefinition( "--classdec", "classdec" ) },
                {OPT.NAMESPACE,           new OptionDefinition( "-a", "namespace" ) },
                {OPT.DECLARATIONS,        new OptionDefinition( "--declarations", "declarations" ) },
                {OPT.PROPERTY_SECTION,    new OptionDefinition( "--propSection", "propSection" ) },
                {OPT.CLASS_SECTION,       new OptionDefinition( "--classSection", "classSection" ) },
                {OPT.INDIVIDUALS_SECTION, new OptionDefinition( "--individualsSection", "individualsSection" ) },
                {OPT.DATATYPES_SECTION,   new OptionDefinition( "--datatypesSection", "datatypesSection" ) },
                {OPT.NOPROPERTIES,        new OptionDefinition( "--noproperties", "noproperties" ) },
                {OPT.NOCLASSES,           new OptionDefinition( "--noclasses", "noclasses" ) },
                {OPT.NOINDIVIDUALS,       new OptionDefinition( "--noindividuals", "noindividuals" ) },
                {OPT.NODATATYPES,         new OptionDefinition( "--nodatatypes", "nodatatypes" ) },
                {OPT.PROP_TEMPLATE,       new OptionDefinition( "--propTemplate", "propTemplate" ) },
                {OPT.CLASS_TEMPLATE,      new OptionDefinition( "--classTemplate", "classTemplate" ) },
                {OPT.INDIVIDUAL_TEMPLATE, new OptionDefinition( "--individualTemplate", "individualTemplate" ) },
                {OPT.DATATYPE_TEMPLATE,   new OptionDefinition( "--datatypeTemplate", "datatypeTemplate" ) },
                {OPT.UC_NAMES,            new OptionDefinition( "--uppercase", "uppercase" ) },
                {OPT.INCLUDE,             new OptionDefinition( "--include", "include" ) },
                {OPT.CLASSNAME_SUFFIX,    new OptionDefinition( "--classnamesuffix", "classnamesuffix" )},
                {OPT.NOHEADER,            new OptionDefinition( "--noheader", "noheader" )},
                {OPT.ENCODING,            new OptionDefinition( "-e", "encoding" )},
                {OPT.HELP,                new OptionDefinition( "--help", "help" )},
                {OPT.DOS,                 new OptionDefinition( "--dos", "dos" )},
                {OPT.USE_INF,             new OptionDefinition( "--inference", "inference" )},
                {OPT.STRICT_INDIVIDUALS,  new OptionDefinition( "--strictIndividuals", "strictIndividuals" )},
                {OPT.INCLUDE_SOURCE,      new OptionDefinition( "--includeSource", "includeSource" )},
                {OPT.NO_STRICT,           new OptionDefinition( "--nostrict", "noStrict")},
            };

        public boolean hasConfigFileOption();
        public String getConfigFileOption();
        public boolean hasRootOption();
        public String getRootOption();
        public boolean hasNoCommentsOption();
        public String getNoCommentsOption();
        public boolean hasInputOption();
        public Resource getInputOption();
        public boolean hasLangOption();
        public String getLangOption();
        public boolean hasLangOwlOption();
        public String getLangOwlOption();
        public boolean hasLangRdfsOption();
        public String getLangRdfsOption();
        public boolean hasOutputOption();
        public String getOutputOption();
        public boolean hasHeaderOption();
        public String getHeaderOption();
        public boolean hasFooterOption();
        public String getFooterOption();
        public boolean hasMarkerOption();
        public String getMarkerOption();
        public boolean hasPackagenameOption();
        public String getPackagenameOption();
        public boolean hasOntologyOption();
        public String getOntologyOption();
        public boolean hasClassnameOption();
        public String getClassnameOption();
        public boolean hasClassdecOption();
        public String getClassdecOption();
        public boolean hasNamespaceOption();
        public Resource getNamespaceOption();
        public boolean hasDeclarationsOption();
        public String getDeclarationsOption();
        public boolean hasPropertySectionOption();
        public String getPropertySectionOption();
        public boolean hasClassSectionOption();
        public String getClassSectionOption();
        public boolean hasIndividualsSectionOption();
        public String getIndividualsSectionOption();
        public boolean hasDatatypesSectionOption();
        public String getDatatypesSectionOption();
        public boolean hasNopropertiesOption();
        public boolean hasNoclassesOption();
        public boolean hasNoindividualsOption();
        public boolean hasNodatatypesOption();
        public boolean hasPropTemplateOption();
        public String getPropTemplateOption();
        public boolean hasClassTemplateOption();
        public String getClassTemplateOption();
        public boolean hasIndividualTemplateOption();
        public String getIndividualTemplateOption();
        public boolean hasDatatypeTemplateOption();
        public String getDatatypeTemplateOption();
        public boolean hasUcNamesOption();
        public boolean hasIncludeOption();
        public List<String> getIncludeOption();
        public boolean hasClassnameSuffixOption();
        public String getClassnameSuffixOption();
        public boolean hasNoheaderOption();
        public boolean hasEncodingOption();
        public String getEncodingOption();
        public boolean hasHelpOption();
        public String getHelpOption();
        public boolean hasDosOption();
        public boolean hasUseInfOption();
        public boolean hasStrictIndividualsOption();
        public boolean hasIncludeSourceOption();
        public boolean hasNoStrictOption();
    }

    public static class SchemagenOptionsImpl
        implements SchemagenOptions
    {
        // Instance variables
        /** The list of command line arguments */
        private List<String> m_cmdLineArgs = new ArrayList<>();

        /** The root of the options in the config file */
        private Resource m_root;

        /** The model that contains the configuration information */
        private Model m_config = ModelFactory.createDefaultModel();

        // Constructor

        public SchemagenOptionsImpl( String[] args ) {
            m_cmdLineArgs = Arrays.asList( args );

            // check to see if there's a specified config file
            String configURL = DEFAULT_CONFIG_URI;
            if (hasConfigFileOption()) {
                // check for protocol; add file: if not specified
                configURL = SchemagenUtils.urlCheck( getConfigFileOption() );
            }

            // try to read the config URI
            try {
                RDFDataMgr.read( m_config, configURL );
            }
            catch (Exception e) {
                // if the user left the default config URI in place, it's not an error to fail to read it
                if (!configURL.equals( DEFAULT_CONFIG_URI )) {
                    throw new SchemagenException( "Failed to read configuration from URL: " + configURL, e );
                }
            }

            // ensure we have a root URI for the configuration model
            determineConfigRoot();
        }

        /**
         * Return the configuration model used to hold config information
         * @return Model
         */
        protected Model getConfigModel() {
            return m_config;
        }

        /**
         * Return the root resource to which configuration information is attached
         * @return Resource
         */
        protected Resource getConfigRoot() {
            if (m_root == null) {
                determineConfigRoot();
            }
            return m_root;
        }

        // Internal implementation methods

        /** Determine the root resource in the configuration file */
        protected void determineConfigRoot() {
            if (hasValue( OPT.ROOT )) {
                m_root = m_config.getResource( getStringValue( OPT.ROOT ) );
            }
            else {
                // no specified root, we assume there is only one with type sgen:Config
                StmtIterator i = m_config.listStatements( null, RDF.type, m_config.getResource( NS + "Config" ) );
                if (i.hasNext()) {
                    m_root = i.nextStatement().getSubject();
                }
                else {
                    // no configuration root, so we invent one
                    m_root = m_config.createResource();
                }
            }
        }

        /** Answer true if the given option is set to true */
        protected boolean isTrue( OPT option ) {
            return getOpt( option ).isTrue( m_cmdLineArgs, m_root );
        }

        /** Answer true if the given option has value */
        protected boolean hasValue( OPT option ) {
            return getOpt( option ).hasValue( m_cmdLineArgs, m_root );
        }

        /** Answer the value of the option or null */
        protected RDFNode getValue( OPT option ) {
            return getOpt( option ).getValue( m_cmdLineArgs, m_root );
        }

        /** Answer the String value of the option or null */
        protected String getStringValue( OPT option ) {
            return getOpt( option ).getStringValue( m_cmdLineArgs, m_root );
        }

        /** Answer true if the given option has a resource value */
        protected boolean hasResourceValue( OPT option ) {
            return getOpt( option ).hasResourceValue( m_cmdLineArgs, m_root );
        }

        /** Answer the value of the option or null */
        protected Resource getResource( OPT option ) {
            return getOpt( option ).getResource( m_cmdLineArgs, m_root );
        }

        /** Answer all values for the given options as Strings */
        protected List<String> getAllValues( OPT option ) {
            List<String> values = new ArrayList<>();
            OptionDefinition opt = getOpt( option );

            // look in the command line arguments
            for (Iterator<String> i = m_cmdLineArgs.iterator(); i.hasNext(); ) {
                String s = i.next();
                if (s.equals( opt.m_cmdLineForm )) {
                    // next iterator value is the arg value
                    values.add( i.next() );
                }
            }

            // now look in the config file
            for (StmtIterator i = m_root.listProperties( opt.m_prop ); i.hasNext(); ) {
                Statement s = i.nextStatement();

                if (s.getObject() instanceof Literal) {
                    values.add( s.getString() );
                }
                else {
                    values.add( s.getResource().getURI() );
                }
            }

            return values;
        }

        /** Answer the option object for the given option */
        protected OptionDefinition getOpt( OPT option ) {
            for ( Object[] m_optionDefinition : m_optionDefinitions )
            {
                if ( m_optionDefinition[0] == option )
                {
                    return (OptionDefinition) m_optionDefinition[1];
                }
            }

            return null;
        }

        // External interface methods

        @Override
        public boolean hasConfigFileOption() { return hasValue( OPT.CONFIG_FILE ); }
        @Override
        public String getConfigFileOption() { return getStringValue( OPT.CONFIG_FILE ); }
        @Override
        public boolean hasRootOption() { return hasValue( OPT.ROOT ); }
        @Override
        public String getRootOption() { return getStringValue( OPT.ROOT ); }
        @Override
        public boolean hasNoCommentsOption() { return isTrue( OPT.NO_COMMENTS ); }
        @Override
        public String getNoCommentsOption() { return getStringValue( OPT.NO_COMMENTS ); }
        @Override
        public boolean hasInputOption() { return hasValue( OPT.INPUT ); }
        @Override
        public Resource getInputOption() { return getResource( OPT.INPUT ); }
        @Override
        public boolean hasLangOption() { return hasValue( OPT.LANG ); }
        @Override
        public String getLangOption() { return getStringValue( OPT.LANG ); }
        @Override
        public boolean hasLangOwlOption() { return isTrue( OPT.LANG_OWL ); }
        @Override
        public String getLangOwlOption() { return getStringValue( OPT.LANG_OWL ); }
        @Override
        public boolean hasLangRdfsOption() { return isTrue( OPT.LANG_RDFS ); }
        @Override
        public String getLangRdfsOption() { return getStringValue( OPT.LANG_RDFS ); }
        @Override
        public boolean hasOutputOption() { return hasValue( OPT.OUTPUT ); }
        @Override
        public String getOutputOption() { return getStringValue( OPT.OUTPUT ); }
        @Override
        public boolean hasHeaderOption() { return hasValue( OPT.HEADER ); }
        @Override
        public String getHeaderOption() { return getStringValue( OPT.HEADER ); }
        @Override
        public boolean hasFooterOption() { return isTrue( OPT.FOOTER ); }
        @Override
        public String getFooterOption() { return getStringValue( OPT.FOOTER ); }
        @Override
        public boolean hasMarkerOption() { return hasValue( OPT.MARKER ); }
        @Override
        public String getMarkerOption() { return getStringValue( OPT.MARKER ); }
        @Override
        public boolean hasPackagenameOption() { return hasValue( OPT.PACKAGENAME ); }
        @Override
        public String getPackagenameOption() { return getStringValue( OPT.PACKAGENAME ); }
        @Override
        public boolean hasOntologyOption() { return isTrue( OPT.ONTOLOGY ); }
        @Override
        public String getOntologyOption() { return getStringValue( OPT.ONTOLOGY ); }
        @Override
        public boolean hasClassnameOption() { return hasValue( OPT.CLASSNAME ); }
        @Override
        public String getClassnameOption() { return getStringValue( OPT.CLASSNAME ); }
        @Override
        public boolean hasClassdecOption() { return hasValue( OPT.CLASSDEC ); }
        @Override
        public String getClassdecOption() { return getStringValue( OPT.CLASSDEC ); }
        @Override
        public boolean hasNamespaceOption() { return hasValue( OPT.NAMESPACE ); }
        @Override
        public Resource getNamespaceOption() { return getResource( OPT.NAMESPACE ); }
        @Override
        public boolean hasDeclarationsOption() { return hasValue( OPT.DECLARATIONS ); }
        @Override
        public String getDeclarationsOption() { return getStringValue( OPT.DECLARATIONS ); }
        @Override
        public boolean hasPropertySectionOption() { return hasValue( OPT.PROPERTY_SECTION ); }
        @Override
        public String getPropertySectionOption() { return getStringValue( OPT.PROPERTY_SECTION ); }
        @Override
        public boolean hasClassSectionOption() { return hasValue( OPT.CLASS_SECTION ); }
        @Override
        public String getClassSectionOption() { return getStringValue( OPT.CLASS_SECTION ); }
        @Override
        public boolean hasIndividualsSectionOption() { return hasValue( OPT.INDIVIDUALS_SECTION ); }
        @Override
        public String getIndividualsSectionOption() { return getStringValue( OPT.INDIVIDUALS_SECTION ); }
        @Override
        public boolean hasDatatypesSectionOption() { return hasValue( OPT.DATATYPES_SECTION ); }
        @Override
        public String getDatatypesSectionOption() { return getStringValue( OPT.DATATYPES_SECTION ); }
        @Override
        public boolean hasNopropertiesOption() { return isTrue( OPT.NOPROPERTIES ); }
        @Override
        public boolean hasNoclassesOption() { return isTrue( OPT.NOCLASSES ); }
        @Override
        public boolean hasNoindividualsOption() { return isTrue( OPT.NOINDIVIDUALS ); }
        @Override
        public boolean hasNodatatypesOption() { return isTrue( OPT.NODATATYPES ); }
        @Override
        public boolean hasPropTemplateOption() { return hasValue( OPT.PROP_TEMPLATE ); }
        @Override
        public String getPropTemplateOption() { return getStringValue( OPT.PROP_TEMPLATE ); }
        @Override
        public boolean hasClassTemplateOption() { return hasValue( OPT.CLASS_TEMPLATE ); }
        @Override
        public String getClassTemplateOption() { return getStringValue( OPT.CLASS_TEMPLATE ); }
        @Override
        public boolean hasIndividualTemplateOption() { return hasValue( OPT.INDIVIDUAL_TEMPLATE ); }
        @Override
        public String getIndividualTemplateOption() { return getStringValue( OPT.INDIVIDUAL_TEMPLATE ); }
        @Override
        public boolean hasDatatypeTemplateOption() { return hasValue( OPT.DATATYPE_TEMPLATE ); }
        @Override
        public String getDatatypeTemplateOption() { return getStringValue( OPT.DATATYPE_TEMPLATE ); }
        @Override
        public boolean hasUcNamesOption() { return isTrue( OPT.UC_NAMES ); }
        @Override
        public boolean hasIncludeOption() { return hasValue( OPT.INCLUDE ); }
        @Override
        public List<String> getIncludeOption() { return getAllValues( OPT.INCLUDE ); }
        @Override
        public boolean hasClassnameSuffixOption() { return hasValue( OPT.CLASSNAME_SUFFIX ); }
        @Override
        public String getClassnameSuffixOption() { return getStringValue( OPT.CLASSNAME_SUFFIX ); }
        @Override
        public boolean hasNoheaderOption() { return isTrue( OPT.NOHEADER ); }
        @Override
        public boolean hasEncodingOption() { return hasValue( OPT.ENCODING ); }
        @Override
        public String getEncodingOption() { return getStringValue( OPT.ENCODING ); }
        @Override
        public boolean hasHelpOption() { return hasValue( OPT.HELP ); }
        @Override
        public String getHelpOption() { return getStringValue( OPT.HELP ); }
        @Override
        public boolean hasDosOption() { return isTrue( OPT.DOS ); }
        @Override
        public boolean hasUseInfOption() { return isTrue( OPT.USE_INF ); }
        @Override
        public boolean hasStrictIndividualsOption() { return isTrue( OPT.STRICT_INDIVIDUALS ); }
        @Override
        public boolean hasIncludeSourceOption() { return isTrue( OPT.INCLUDE_SOURCE ); }
        @Override
        public boolean hasNoStrictOption() { return isTrue( OPT.NO_STRICT ); }
    }

    /** An option that can be set either on the command line or in the RDF config */
    public static class OptionDefinition
    {
        protected String m_cmdLineForm;
        protected Property m_prop;

        protected OptionDefinition( String cmdLineForm, String name ) {
            m_cmdLineForm = cmdLineForm;
            if (name != null) {
                m_prop = ResourceFactory.createProperty( NS, name );
            }
        }

        /**
         * Return the RDF property that is used when configuring this option
         * via a {@link Model}
         * @return The declaration property, or null
         */
        public Property getDeclarationProperty() {
            return m_prop;
        }

        /**
         * Return the command line form of this option
         * @return The command line form as a String
         */
        public String getCommandLineForm() {
            return m_cmdLineForm;
        }

        /**
         * Answer true if this option is set to true, either on the command line
         * or in the config model
         *
         * @return boolean
         */
        protected boolean isTrue( List<String> cmdLineArgs, Resource confRoot ) {
            if (cmdLineArgs.contains( m_cmdLineForm )) {
                return true;
            }

            if (confRoot.hasProperty( m_prop )) {
                return confRoot.getRequiredProperty( m_prop ).getBoolean();
            }

            return false;
        }

        /**
         * Answer the string value of the parameter if set, or null otherwise. Note command line
         * has precedence.
         *
         * @return String
         */
        protected String getStringValue( List<String> cmdLineArgs, Resource confRoot ) {
            RDFNode n = getValue( cmdLineArgs, confRoot );
            return (n == null) ? null : (n.isLiteral() ? n.asLiteral().getLexicalForm() : n.toString() );
        }

        /**
         * Return the value of the parameter if set, or null otherwise. Note command line
         * has precedence.
         *
         * @return The argument value as an RDFNode
         */
        protected RDFNode getValue( List<String> cmdLineArgs, Resource confRoot ) {
            int index = cmdLineArgs.indexOf( m_cmdLineForm );

            if (index >= 0) {
                try {
                    return ResourceFactory.createPlainLiteral( cmdLineArgs.get( index + 1 ) );
                }
                catch (IndexOutOfBoundsException e) {
                    throw new SchemagenException( "Value for parameter " + m_cmdLineForm  + " not set! Aborting.", e );
                }
            }

            if (m_prop != null && confRoot != null  &&  confRoot.hasProperty( m_prop )) {
                return confRoot.getRequiredProperty( m_prop ).getObject();
            }

            // not set
            return null;
        }

        /**
         * Answer true if the parameter has a value at all.
         *
         * @return boolean
         */
        protected boolean hasValue( List<String> cmdLineArgs, Resource confRoot ) {
            return getValue( cmdLineArgs, confRoot ) != null;
        }


        /**
         * Answer the resource value of the parameter if set, or null otherwise.
         *
         * @return String
         */
        protected Resource getResource( List<String> cmdLineArgs, Resource confRoot ) {
            int index = cmdLineArgs.indexOf( m_cmdLineForm );

            if (index >= 0) {
                try {
                    return confRoot.getModel().getResource( cmdLineArgs.get( index + 1 ) );
                }
                catch (IndexOutOfBoundsException e) {
                    System.err.println( "Value for parameter " + m_cmdLineForm  + " not set! Aborting.");
                }
            }

            if (m_prop != null  &&  confRoot.hasProperty( m_prop )) {
                return confRoot.getRequiredProperty( m_prop ).getResource();
            }

            // not set
            return null;
        }

        /**
         * Answer true if the parameter has a value at all.
         *
         * @return boolean
         */
        protected boolean hasResourceValue( List<String> cmdLineArgs, Resource confRoot ) {
            return getResource( cmdLineArgs, confRoot ) != null;
        }
    }  // end inner class OptionDefinition


    /** A pairing of pattern and substitution we want to apply to output */
    protected class Replacement
    {
        protected String sub;
        protected Pattern pattern;

        protected Replacement( Pattern pattern, String sub) {
            this.sub = sub;
            this.pattern = pattern;
        }
    } // end inner class Replacement

    /**
     * <p>Schemagen runtime exception</p>
     */
    public static class SchemagenException
        extends RuntimeException
    {
        public SchemagenException( String msg, Throwable cause ) {
            super( msg, cause );
        }
    }

    /** Utility method container */
    public static class SchemagenUtils
    {
        /** Return a URI formed from the given string, unchanged if it's already a URI or
         *  converted to a file URI otherwise.  If not recognisable as a URL, abort.
         */
        public static String urlCheck( String uriOrFile ) {
            boolean legal = true;
            // is it a URI already?  to check, we make a URL and see what happens!
            try {
                new URI(uriOrFile).toURL();
                return uriOrFile;
            }
            catch (URISyntaxException | MalformedURLException | IllegalArgumentException ignore) {
                legal = false;
            }

            String url = uriOrFile;
            // if not a legal url, assume it's a file
            if (!legal) {
                legal = true;
                String slash = System.getProperty( "file.separator" );
                url = "file:" + (uriOrFile.startsWith( slash ) ? (slash + slash) : "") + uriOrFile;

                try {
                    new URI(url).toURL();
                }
                catch (URISyntaxException | MalformedURLException ignore) {
                    legal = false;
                }
            }

            if (!legal) {
                throw new SchemagenException( "Could not parse " + uriOrFile + " as a legal URL or a file reference. Aborting.", null );
            }
            return url;
        }
    } /* End class SchemagenUtils */
}
