org.outofbits.sesame.schemagen.SchemaGeneration.java Source code

Java tutorial

Introduction

Here is the source code for org.outofbits.sesame.schemagen.SchemaGeneration.java

Source

/* MIT License - Copyright (c) 2016 Kevin Haller <kevin.haller@outofbits.com> */
package org.outofbits.sesame.schemagen;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.eclipse.rdf4j.model.*;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.*;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.RDFParseException;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.rio.UnsupportedRDFormatException;
import org.outofbits.sesame.schemagen.exception.RDFFormatNotSupportedException;
import org.outofbits.sesame.schemagen.exception.SchemaGenerationException;
import org.outofbits.sesame.schemagen.exception.VocabularyNotFoundException;
import org.outofbits.sesame.schemagen.utils.JavaIdentifier;
import org.outofbits.sesame.schemagen.utils.StringMapFormatter;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * This is the core class of Sesame-Schemagen and provides the capability to generate a Java
 * class from given {@link VocabularyOptions} with the given input document. In order to start the
 * generation process {@code generate()} must be called.
 *
 * @author Kevin Haller <kevin.haller@outofbits.com>
 * @version 1.0
 * @since 1.0
 */
public class SchemaGeneration {

    /* User agent string for HTTP requests. */
    private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Sesame-SchemaGen; version 1.0) Gecko/20100101 Firefox/10.0";

    /* Templates */
    private static final String DEFAULT_GLOBAL_TEMPLATE = "%(header)s%n%(class)s";

    private static final String DEFAULT_HEADER_TEMPLATE = "%(package)s%n%(imports)s%n%(class-comment)s";

    private static final String DEFAULT_CLASS_COMMENT_TEMPLATE = "/**%n * %(vocab-label)s%n * <p/>%n * %(vocab-comment$80$%n * )w%n *%n"
            + " * @author Auto-generated by Sesame-Schemagen on %(generation-date)s%n"
            + " * @version %(vocab-version-info)s%n * @see <a href=\"%(namespace)s\">%(vocab-label)s</a>%n */";

    private static final String DEFAULT_CLASS_TEMPLATE = "public final class %(class-name)s {%n%n%(class-body)s%n}%n";

    private static final String DEFAULT_CLASS_BODY_TEMPLATE = "%(namespace)s%(prefix)s%(resource-declarations)s%n%istatic {%n%(resource-definitions)s%n%i}";

    private static final String DEFAULT_NAMESPACE_TEMPLATE = "%i/** Describes the base namespace of this vocabulary */%n%ipublic static final String NS = \"%(namespace-iri)s\";%n%n";

    private static final String DEFAULT_PREFIX_TEMPLATE = "%i/** Describes the preferred prefix of this vocabulary. */%n%ipublic static final String PREFIX = \"%(prefix)s\";%n%n";

    private static final String DEFAULT_RESOURCE_DEC_TEMPLATE = "%n%i/**%n%i * %(resource-label$80$%n%i * )w%n%i * <p/>%n%i * %(resource-comment$80$%n%i * )w%n%i * %n%i * @see <a href=\"%(resource-iri)s\">%(resource-name)s</a>%n%i */%n%ipublic static final %(sesame-resource-class)s %(resource-name)s;%n";

    private static final String DEFAULT_VALUE_FACTORY_TEMPLATE = "%(2)iValueFactory valueFactory = %(sesame-value-factory)s.getInstance();%n%n";

    private static final String DEFAULT_RESOURCE_DEF_TEMPLATE = "%(2)i%(resource-name)s = valueFactory.create%(sesame-resource-class)s(\"%(resource-iri)s\");%n";

    /**
     * All well-known properties that give a short description of a resource.
     */
    public static final IRI[] LABEL_PROPERTIES = new IRI[] { RDFS.LABEL, DCTERMS.TITLE, DC.TITLE };

    /**
     * All well-known properties that could describe a resource.
     */
    public static final IRI[] COMMENT_PROPERTIES = new IRI[] { RDFS.COMMENT, DCTERMS.DESCRIPTION, DCTERMS.ABSTRACT,
            DC.DESCRIPTION };

    /**
     * All well-known properties that could indicate the resource describing the vocabulary.
     */
    public static final IRI[] VOCAB_CLASSES = new IRI[] { OWL.ONTOLOGY };

    /**
     * All well-known properties that could describe the version of the vocabulary.
     */
    public static final IRI[] VOCAB_VERSION_PROPERTIES = new IRI[] { OWL.VERSIONINFO, OWL.VERSIONIRI };

    /**
     * A map that links the name of formats to the corresponding {@link RDFFormat} instance.
     */
    private static final Map<String, RDFFormat> supportedRDFFormats = new HashMap<>();

    static {
        supportedRDFFormats.put(RDFFormat.RDFXML.getName().toLowerCase(), RDFFormat.RDFXML);
        supportedRDFFormats.put(RDFFormat.TURTLE.getName().toLowerCase(), RDFFormat.TURTLE);
        supportedRDFFormats.put(RDFFormat.JSONLD.getName().toLowerCase(), RDFFormat.JSONLD);
    }

    /**
     * Checks if {@link SchemaGeneration} supports the given format in form of a string.
     *
     * @param rdfFormatString the rdf format in form of a string.
     * @return true, if {@link SchemaGeneration} supports the given format, otherwise false.
     */
    public static boolean supportsRDFFormat(String rdfFormatString) {
        return supportedRDFFormats.containsKey(rdfFormatString.toLowerCase());
    }

    /**
     * Gets the corresponding {@link RDFFormat} of the given format in form of a string. A
     * {@link RDFFormatNotSupportedException} will be thrown, if the given format is not supported
     * by {@link SchemaGeneration}. {@code supportsRDFFormat(String rdfFormatString)} can be used to
     * avoid the exception beforehand.
     *
     * @param rdfFormatString the format in form a string for which the corresponding
     *                        {@link RDFFormat} shall be returned.
     * @return the corresponding {@link RDFFormat} of the given format in form of a string.
     * @throws RDFFormatNotSupportedException if the given format is not supported.
     */
    public static RDFFormat rdfFormatOf(String rdfFormatString) throws RDFFormatNotSupportedException {
        if (!supportsRDFFormat(rdfFormatString)) {
            throw new RDFFormatNotSupportedException(
                    String.format("The given format '%s' is not supported by Schemagen. Supported formats are %s.",
                            rdfFormatString, supportedRDFFormats.keySet()));
        }
        return supportedRDFFormats.get(rdfFormatString);
    }

    private ValueFactory valueFactory = SimpleValueFactory.getInstance();
    private VocabularyOptions vocabularyOptions;
    private String[] sesamePackagesToImport;

    private StringMapFormatter stringMapFormatter;
    private Map<String, String> formattingMap = new HashMap<>();

    /**
     * Creates a new instance of {@link SchemaGeneration} by considering the given
     * {@link VocabularyOptions}.
     *
     * @param vocabularyOptions the {@link VocabularyOptions} that shall be considered for the
     *                          schema generation.
     */
    public SchemaGeneration(VocabularyOptions vocabularyOptions) {
        this.vocabularyOptions = vocabularyOptions;
        this.sesamePackagesToImport = vocabularyOptions.sesameLowerThanV4()
                ? new String[] { "model.URI", "model.ValueFactory", "model.impl.ValueFactoryImpl" }
                : new String[] { "model.IRI", "model.ValueFactory", "model.impl.SimpleValueFactory" };
        this.stringMapFormatter = new StringMapFormatter();
        // Initializes the global formatting map.
        formattingMap.put("class-name", JavaIdentifier.getJavaIdFrom(vocabularyOptions.className()));
        formattingMap.put("generation-date", new SimpleDateFormat("MM/dd/yyyy").format(new Date()));
        formattingMap.put("sesame-package-base",
                vocabularyOptions.rdfFramework().equals(RDFFramework.RDF4J) ? "org.eclipse.rdf4j" : "org.openrdf");
        formattingMap.put("sesame-resource-class", vocabularyOptions.sesameLowerThanV4() ? "URI" : "IRI");
        formattingMap.put("sesame-value-factory",
                vocabularyOptions.sesameLowerThanV4() ? "ValueFactoryImpl" : "SimpleValueFactory");
    }

    /**
     * Gets the vocabulary the given input is pointing to. The input can either be a file path or
     * a HTTP {@link java.net.URL}, otherwise a {@link VocabularyNotFoundException} will be thrown.
     * Also if the vocabulary cannot be accessed or read, a {@link VocabularyNotFoundException} will
     * be thrown.
     *
     * @param input      path to a file or a valid {@link java.net.URL}. Schemagen supports the
     *                   protocols 'http' and 'file'. Input must not be null.
     * @param format     the format of the document representing the vocabulary. The format can be
     *                   null.
     * @param workingDir the working directory.
     * @return {@link Model} containing all statements of the vocabulary pointed to by input.
     * @throws VocabularyNotFoundException if the vocabulary cannot be found or accessed.
     */
    private Model getVocabularyModel(String input, RDFFormat format, String workingDir)
            throws VocabularyNotFoundException {
        assert input != null;
        try {
            URL documentUrl = new URL(input);
            switch (documentUrl.getProtocol()) {
            case "file":
                File sourceFile = new File(documentUrl.getFile());
                if (sourceFile.isAbsolute()) {
                    return readVocabularyFromFile(sourceFile, format);
                } else {
                    return readVocabularyFromFile(new File(workingDir, sourceFile.getPath()), format);
                }
            case "http":
                return readVocabularyFromHTTPSource(documentUrl, format);
            default:
                throw new VocabularyNotFoundException(
                        String.format("The protocol '%s' is not supported.", documentUrl.getProtocol()));
            }
        } catch (MalformedURLException e) {
            try {
                File sourceFile = new File(input);
                if (sourceFile.isAbsolute()) {
                    return readVocabularyFromFile(sourceFile, format);
                } else {
                    return readVocabularyFromFile(new File(workingDir, sourceFile.getPath()), format);
                }
            } catch (IOException io) {
                throw new VocabularyNotFoundException(e);
            }
        } catch (Exception e) {
            throw new VocabularyNotFoundException(e);
        }
    }

    /**
     * Requests the RDF file of the vocabulary by using the given HTTP {@link java.net.URL}. The
     * header-entry ACCEPT contains all supported {@link RDFFormat}s, where the given
     * {@link RDFFormat} is the preferred one. The responded rdf file will be parsed and a
     * {@link Model} containing all statements will be returned. An {@link IOException} will be
     * thrown, if the rdf file cannot be accessed or read. {@link URISyntaxException} will be thrown,
     * if the given {@link java.net.URL} has a syntax error.
     *
     * @param url    the url, where the vocabulary is located and accessible by using HTTP. It must
     *               not be null.
     * @param format the format of the document representing the vocabulary. The format can be null.
     * @return {@link Model} containing all statements of the vocabulary located at the given
     * {@link java.net.URL}.
     * @throws IOException        if the rdf file cannot be accessed or read.
     * @throws URISyntaxException if the given {@link java.net.URL} has a syntax error.
     */
    private Model readVocabularyFromHTTPSource(java.net.URL url, RDFFormat format)
            throws IOException, URISyntaxException {
        assert url != null && url.getProtocol().equals("http");
        HttpClientBuilder clientBuilder = HttpClientBuilder.create().setUserAgent(HTTP_USER_AGENT);
        HttpUriRequest vocabularyGETRequest = RequestBuilder.get().setUri(url.toURI())
                .setHeader(HttpHeaders.ACCEPT,
                        String.join(", ", RDFFormat.getAcceptParams(supportedRDFFormats.values(), false, format)))
                .build();
        try (CloseableHttpClient client = clientBuilder.build()) {
            CloseableHttpResponse response = client.execute(vocabularyGETRequest);
            if (response.getStatusLine().getStatusCode() != 200) {
                throw new IOException(String.format("The given vocabulary can not requested from '%s'. %d: %s", url,
                        response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()));
            }
            String responseContentType = response.getEntity().getContentType().getValue();
            Optional<RDFFormat> optionalResponseRdfFormat = Rio.getParserFormatForMIMEType(responseContentType);
            try (InputStream vocabResponseStream = response.getEntity().getContent()) {
                if (optionalResponseRdfFormat.isPresent()) {
                    return Rio.parse(vocabResponseStream, "", optionalResponseRdfFormat.get());
                } else {
                    if (format == null) {
                        throw new IOException(
                                String.format("The returned content type (%s) from '%s' is not supported",
                                        responseContentType, url));
                    }
                    try {
                        return Rio.parse(vocabResponseStream, "", format);
                    } catch (RDFParseException | UnsupportedRDFormatException e) {
                        throw new IOException(String.format(
                                "The returned content type (%s) from '%s' is not supported. Fallback to the given format %s, but an error occurred.",
                                responseContentType, url, format), e);
                    }
                }
            }
        }
    }

    /**
     * Reads the given file that contains the vocabulary and stores the statements in a
     * {@link Model} that will be returned. A {@link FileNotFoundException} will be thrown, if the
     * given file does not exist. Important for interpreting the given file is the used
     * {@link RDFFormat}. The format can either be given explicitly or be null. If the given
     * {@link RDFFormat} is null, this method will try to detect the format. In case of a failed
     * detection {@code RDFFormat.RDFXML} will be used per default.
     *
     * @param vocabularyFile the {@link File} that represents the vocabulary in the given format.
     *                       The file must not be null.
     * @param format         the {@link RDFFormat} of the given {@link File}.
     * @return {@link Model} containing the vocabulary.
     * @throws IOException if an io error occurs. f.e. the given file does not exist.
     */
    private Model readVocabularyFromFile(File vocabularyFile, RDFFormat format) throws IOException {
        assert vocabularyFile != null;
        if (!vocabularyFile.exists()) {
            throw new IOException(String.format("%s cannot be found at '%s'.", vocabularyFile.getName(),
                    vocabularyFile.getAbsolutePath()));
        }
        if (format == null) {
            Optional<RDFFormat> optionalFileFormat = Rio.getParserFormatForFileName(vocabularyFile.getName());
            if (optionalFileFormat.isPresent()) {
                format = optionalFileFormat.get();
            } else {
                format = RDFFormat.RDFXML;
            }
        }
        try (InputStream inputStream = new BufferedInputStream(new FileInputStream(vocabularyFile))) {
            return Rio.parse(inputStream, "", format);
        }
    }

    /**
     * Prepares for the class file that shall be generated.
     *
     * @return {@link File} for the generated Java class.
     */
    private File prepareFileForGeneration() {
        File outputDirectory = new File(vocabularyOptions.outputDir());
        File packageDirectory = vocabularyOptions.classPackage() != null
                ? new File(outputDirectory.getAbsolutePath(), vocabularyOptions.classPackage().replace(".", "/"))
                : outputDirectory;
        packageDirectory.mkdirs();
        return new File(packageDirectory.getAbsolutePath(),
                JavaIdentifier.getJavaIdFrom(vocabularyOptions.className()) + ".java");
    }

    /**
     * Gets the root resource of the given vocabulary, or null if n such reosurce could be found.
     *
     * @param vocabulary {@link Model} containing all statements of the vocabulary.
     * @return the root {@link Resource} or null, if no such resource could be found.
     * @throws SchemaGenerationException if there are more than one root resource.
     */
    private Resource getRootResource(Model vocabulary) throws SchemaGenerationException {
        assert vocabulary != null;
        Resource vocabularyRootResource = null;
        // Using Namespace for getting the root resource.
        if (vocabularyOptions.baseNamespace() != null) {
            Optional<Resource> optionalVocabRootResource = vocabulary
                    .filter(valueFactory.createIRI(vocabularyOptions.baseNamespace()), null, null).subjects()
                    .stream().findFirst();
            if (optionalVocabRootResource.isPresent()) {
                vocabularyRootResource = optionalVocabRootResource.get();
            }
        }
        if (vocabularyRootResource == null) {
            // Getting the root resource that is an instance of a known vocabulary class.
            for (IRI clazz : VOCAB_CLASSES) {
                Set<Resource> vocabRootResources = vocabulary.filter(null, RDF.TYPE, clazz).subjects();
                if (vocabRootResources.size() > 1) {
                    throw new SchemaGenerationException(String.format("Ambiguous root resources (%s).", String.join(
                            ", ",
                            vocabRootResources.stream().map(Resource::stringValue).collect(Collectors.toList()))));
                } else if (vocabRootResources.size() == 1) {
                    Resource currentVocabularyRootResource = vocabRootResources.iterator().next();
                    if (vocabularyRootResource == null) {
                        vocabularyRootResource = currentVocabularyRootResource;
                    } else if (!currentVocabularyRootResource.equals(vocabularyRootResource)) {
                        throw new SchemaGenerationException(String.format("Ambiguous root resources (%s, %s).",
                                vocabularyRootResource, currentVocabularyRootResource));
                    }
                }
            }
        }
        return vocabularyRootResource;
    }

    /**
     * Gets the first object literal that can be found using the given properties for the given
     * {@link Resource} in the vocabulary {@link Model}.
     *
     * @param vocabulary {@link Model} containing all statements of the vocabulary.
     * @param resource   {@link Resource} for which the first object literal shall be returned.
     * @param properties the properties that shall be used for finding a object literal
     * @return {@link Optional} string value that is potentially empty, if no object literal could
     * be found.
     */
    private Optional<String> getFirstLiteralFor(Model vocabulary, Resource resource, IRI... properties) {
        assert vocabulary != null && resource != null;
        assert properties.length != 0;
        return getFirstLiteralFor(vocabulary, vocabularyOptions.preferredLanguage(), resource, properties);
    }

    /**
     * Gets the first object literal that can be found using the given properties for the given
     * {@link Resource} in the vocabulary {@link Model}.
     *
     * @param vocabulary {@link Model} containing all statements of the vocabulary.
     * @param language   the language that is preferred.
     * @param resource   {@link Resource} for which the first object literal shall be returned.
     * @param properties the properties that shall be used for finding a object literal
     * @return {@link Optional} string value that is potentially empty, if no object literal could
     * be found.
     */
    private Optional<String> getFirstLiteralFor(Model vocabulary, String language, Resource resource,
            IRI... properties) {
        assert vocabulary != null && resource != null;
        assert properties.length != 0;
        for (IRI property : properties) {
            Map<String, String> literalMap = new HashMap<>();
            vocabulary.filter(resource, property, (IRI) null).objects().stream()
                    .filter(objectValue -> objectValue instanceof Literal).forEach(objectValue -> {
                        Literal literal = (Literal) objectValue;
                        Optional<String> optionalLanguageCode = literal.getLanguage();
                        if (optionalLanguageCode.isPresent()) {
                            literalMap.put(optionalLanguageCode.get(), literal.getLabel());
                        } else {
                            literalMap.put(null, literal.getLabel());
                        }
                    });
            if (literalMap.containsKey(language)) {
                return Optional.of(literalMap.get(language));
            } else if (literalMap.containsKey(null)) {
                return Optional.of(literalMap.get(null));
            } else if (literalMap.containsKey("en")) {
                return Optional.of(literalMap.get("en"));
            } else {
                if (!literalMap.isEmpty()) {
                    return Optional.of(literalMap.values().iterator().next());
                }
            }
        }
        return Optional.empty();
    }

    /**
     * Generates the Java class for the given input and options.
     *
     * @throws VocabularyNotFoundException if the vocabulary cannot be found o accessed.
     */
    public void generate() throws VocabularyNotFoundException, SchemaGenerationException {
        generate(".");
    }

    /**
     * Generates the Java class for the given input and options.
     *
     * @param workingDir the working directory.
     * @throws VocabularyNotFoundException if the vocabulary cannot be found o accessed.
     */
    public void generate(String workingDir) throws VocabularyNotFoundException, SchemaGenerationException {
        Model vocabularyModel;
        try {
            vocabularyModel = getVocabularyModel(vocabularyOptions.input(), vocabularyOptions.inputFormat(),
                    workingDir);
        } catch (VocabularyNotFoundException e) {
            if (vocabularyOptions.hasAlternativeInput()) {
                vocabularyModel = getVocabularyModel(vocabularyOptions.alternativeInput(),
                        vocabularyOptions.inputFormat(), workingDir);
            } else {
                throw e;
            }
        }
        Resource vocabularyRootResource = getRootResource(vocabularyModel);
        if (vocabularyRootResource != null) {
            formattingMap
                    .put("vocab-label",
                            getFirstLiteralFor(vocabularyModel, vocabularyOptions.preferredLanguage(),
                                    vocabularyRootResource, LABEL_PROPERTIES)
                                            .orElse(vocabularyOptions.className()));
            formattingMap.put("vocab-comment", getFirstLiteralFor(vocabularyModel,
                    vocabularyOptions.preferredLanguage(), vocabularyRootResource, COMMENT_PROPERTIES).orElse(""));
            formattingMap.put("vocab-version-info",
                    getFirstLiteralFor(vocabularyModel, vocabularyOptions.preferredLanguage(),
                            vocabularyRootResource, VOCAB_VERSION_PROPERTIES).orElse("unknown"));
            formattingMap.put("namespace", vocabularyRootResource.stringValue());
        } else {
            formattingMap.put("vocab-label", vocabularyOptions.className());
            formattingMap.put("vocab-comment", "");
            formattingMap.put("vocab-version-info", "unknown");
            formattingMap.put("namespace",
                    vocabularyOptions.baseNamespace() != null ? vocabularyOptions.baseNamespace() : "#");
        }
        // Output Java class
        File classFile = prepareFileForGeneration();
        try (PrintWriter printWriter = new PrintWriter(new FileOutputStream(classFile))) {
            Map<String, String> valueMap = new HashMap<>(formattingMap);
            valueMap.put("header", generateHeader(vocabularyModel));
            valueMap.put("class", generateClass(vocabularyRootResource, vocabularyModel));
            printWriter.write(stringMapFormatter.format(DEFAULT_GLOBAL_TEMPLATE, valueMap));
        } catch (FileNotFoundException | StringMapFormatter.StringMapFormatterException e) {
            throw new SchemaGenerationException(e);
        }
    }

    /**
     * Generates the header of the Java class that shall be created.
     *
     * @param vocabModel {@link Model} that contains all statements of the vocabulary for which the
     *                   class shall be created.
     * @return the header of the Java class.
     * @throws StringMapFormatter.StringMapFormatterException if the formatting fails.
     */
    private String generateHeader(Model vocabModel) throws StringMapFormatter.StringMapFormatterException {
        assert vocabModel != null;
        Map<String, String> headerValueMap = new HashMap<>(formattingMap);
        // Package
        headerValueMap.put("package",
                vocabularyOptions.classPackage() != null ? stringMapFormatter.format("package %(name)s;%n",
                        Collections.singletonMap("name", vocabularyOptions.classPackage())) : "");
        // Imports
        StringBuilder importsStringBuilder = new StringBuilder();
        Map<String, String> importMap = new HashMap<>(formattingMap);
        for (String packageToImport : sesamePackagesToImport) {
            importMap.put("pack", packageToImport);
            importsStringBuilder
                    .append(stringMapFormatter.format("import %(sesame-package-base)s.%(pack)s;%n", importMap));
        }
        headerValueMap.put("imports", importsStringBuilder.toString());
        // Class comment
        headerValueMap.put("class-comment",
                stringMapFormatter.format(DEFAULT_CLASS_COMMENT_TEMPLATE, formattingMap));
        return stringMapFormatter.format(DEFAULT_HEADER_TEMPLATE, headerValueMap);
    }

    /**
     * Generates the class.
     *
     * @param vocabModel {@link Model} that contains all statements of the vocabulary for which the
     *                   class shall be created.
     * @return the class in form of a string.
     * @throws StringMapFormatter.StringMapFormatterException if the formatting fails.
     */
    private String generateClass(Resource rootResource, Model vocabModel)
            throws StringMapFormatter.StringMapFormatterException, SchemaGenerationException {
        assert vocabModel != null;
        Map<String, String> classValueMap = new HashMap<>(formattingMap);
        classValueMap.put("class-body", generateClassBody(rootResource, vocabModel));
        return stringMapFormatter.format(DEFAULT_CLASS_TEMPLATE, classValueMap);
    }

    /**
     * Tries to detect the common namespace of all subjects of the given vocabulary. If the root
     * resource is given, it will be ignored in the list for detection.
     *
     * @param rootResource the root resource of the vocabulary, or null if unknown.
     * @return detected the common namespace of all subjects of the given vocabulary or null, if
     * no commonalities can be detected.
     */
    private String detectBaseNamespace(Model vocabModel, Resource rootResource) {
        String baseNamespace = StringUtils
                .getCommonPrefix(vocabModel.subjects().stream().filter(resource -> !resource.equals(rootResource))
                        .map(Resource::stringValue).collect(Collectors.toList()).toArray(new String[0]));
        return !baseNamespace.isEmpty() ? baseNamespace : null;
    }

    /**
     * Generates the body of the class that shall be created. At first the base namespace will be
     * detected. If the base namespace is given in the {@link VocabularyOptions}, this namespace
     * will be preferred. An {@link SchemaGenerationException} will be thrown, if the namespace
     * cannot be detected automatically.
     *
     * @param vocabModel {@link Model} that contains all statements of the vocabulary for which the
     *                   class shall be created.
     * @return the body of the class in form of a string.
     * @throws SchemaGenerationException if the body of the class cannot be generated.
     */
    private String generateClassBody(Resource rootResource, Model vocabModel)
            throws StringMapFormatter.StringMapFormatterException, SchemaGenerationException {
        assert vocabModel != null;
        Map<String, String> classBodyValueMap = new HashMap<>(formattingMap);
        // Detect the base namespace
        String basenameSpace;
        if (vocabularyOptions.baseNamespace() != null) {
            basenameSpace = vocabularyOptions.baseNamespace();
        } else {
            basenameSpace = detectBaseNamespace(vocabModel, rootResource);
            if (basenameSpace == null) {
                if (rootResource != null) {
                    basenameSpace = rootResource.stringValue();
                } else {
                    throw new SchemaGenerationException(
                            "The base namespace could not be detected. Please specify it manually.");
                }
            }
        }
        // Detect the preferred prefix
        String prefix = null;
        if (vocabularyOptions.preferredPrefix() != null) {
            prefix = vocabularyOptions.preferredPrefix();
        } else {
            for (Namespace ns : vocabModel.getNamespaces()) {
                if (ns.getName().equals(basenameSpace)) {
                    prefix = ns.getPrefix();
                    break;
                }
            }
        }
        // Declaration of resources
        Pattern nsPattern = Pattern.compile(String.format("%s(.+)", basenameSpace));
        TreeMap<String, Resource> resourceMap = new TreeMap<>();
        for (Resource subject : vocabModel.subjects()) {
            Matcher matcher = nsPattern.matcher(subject.stringValue());
            if (matcher.find()) {
                resourceMap.put(JavaIdentifier.getJavaIdFrom(matcher.group(1)), subject);
            }
        }
        // Definition and declaration of resources.
        StringBuilder declarationStringBuilder = new StringBuilder();
        StringBuilder definitionStringBuilder = new StringBuilder(
                stringMapFormatter.format(DEFAULT_VALUE_FACTORY_TEMPLATE, formattingMap));
        for (Map.Entry<String, Resource> entry : resourceMap.entrySet()) {
            Resource subject = entry.getValue();
            Map<String, String> valueMap = new HashMap<>(formattingMap);
            valueMap.put("resource-name", entry.getKey());
            valueMap.put("resource-iri", subject.stringValue());
            valueMap.put("resource-label",
                    getFirstLiteralFor(vocabModel, subject, LABEL_PROPERTIES).orElse(entry.getKey()));
            valueMap.put("resource-comment",
                    getFirstLiteralFor(vocabModel, subject, COMMENT_PROPERTIES).orElse(""));
            declarationStringBuilder.append(stringMapFormatter.format(DEFAULT_RESOURCE_DEC_TEMPLATE, valueMap));
            definitionStringBuilder.append(stringMapFormatter.format(DEFAULT_RESOURCE_DEF_TEMPLATE, valueMap));
        }
        classBodyValueMap.put("resource-declarations", declarationStringBuilder.toString());
        classBodyValueMap.put("resource-definitions", definitionStringBuilder.toString());
        // Namespace and prefix.
        Map<String, String> valueMap = new HashMap<>(formattingMap);
        valueMap.put("namespace-iri", basenameSpace);
        valueMap.put("prefix", prefix);
        classBodyValueMap.put("namespace", stringMapFormatter.format(DEFAULT_NAMESPACE_TEMPLATE, valueMap));
        classBodyValueMap.put("prefix",
                prefix == null ? "" : stringMapFormatter.format(DEFAULT_PREFIX_TEMPLATE, valueMap));
        return stringMapFormatter.format(DEFAULT_CLASS_BODY_TEMPLATE, classBodyValueMap);
    }
}