Note: information on this page refers to Ceylon 1.2, not to the current release.

Module overrides

Resolving module conflicts

Sometimes module descriptors are not correct, especially when dealing with interop and Maven, but sometimes you may even want to override Ceylon module descriptors at compile-time or run-time.

Maven module descriptors are notoriously unchecked, and so a lot of module descriptors will miss direct dependencies because they happen to compile and run by accident when using Maven, which uses a flat classpath containing all the transitive module dependencies. Ceylon uses ClassLoader isolation, and as such needs module descriptors to be correct. Also Maven does not support the notion of sharing module imports, so if a module A makes types from its imported module B visible to the users of A, the import of B must be made shared. Furthermore, Maven supports module version conflict resolution by design, luck or overrides, while Ceylon uses strict module version imports. And last but not least, Maven modules frequently bundles things that should not be made visible, such as other modules or tests, that you may want to exclude.

For all these reasons, we created an experimental measure that lets you override Maven or Ceylon module descriptors in a single location, called the overrides.xml file, which lets you:

  • define constants and use them in interpolated XML attributes
  • set a module version (for all modules)
  • replace a module by another module (for all modules)
  • remove a module (for all modules)
  • add/remove module dependencies (per module)
  • edit a module dependency, for example making it shared (per module)
  • include/exclude parts of the jar (for example, to exclude certain packages from a jar)

The syntax is as follows:

<overrides>
    <!-- Define a constant to be used in expressions -->
    <define name="restletVersion" value="2.0.10"/>
    <!-- Replace all versions of weld with version 1.1.4.Final -->
    <set groupId="org.jboss.weld" artifactId="weld-osgi-bundle" version="1.1.4.Final"/>
    <!-- Remove all uses of a module -->
    <remove groupId="org.jboss.weld" artifactId="weld-osgi-bundle"/>
    <!-- Edit dependencies of org.restlet.jse:org.restlet/2.0.10 -->
    <artifact groupId="org.restlet.jse" artifactId="org.restlet" version="${restletVersion}">
        <!-- Add/replace a dependency -->
        <add groupId="org.slf4j" artifactId="slf4j-api" version="1.6.1" shared="true"/>
        <!-- Remove a dependency -->
        <remove groupId="org.osgi" artifactId="org.osgi.core" version="4.0.0"/>
        <!-- Share a dependency -->
        <share groupId="org.slf4j" artifactId="slf4j-impl"/>
    </artifact>
    <!-- Replace all uses of org.apache.camel:camel-core/2.9.2 with version 2.10 -->
    <replace groupId="org.apache.camel" artifactId="camel-core" version="2.9.2">
        <with groupId="org.apache.camel" artifactId="camel-core" version="2.10"/>
    </replace>
    <!-- Edit dependencies of org.osgi:org.osgi.core/4.0.0 -->
    <artifact groupId="org.osgi" artifactId="org.osgi.core" version="4.0.0">
        <!-- Only include org/osgi/** and META-INF/** -->
        <filter>
            <!-- You can include or exclude and the matching is in sequence and stops at the first match -->
            <include path="org/osgi/**"/>
            <include path="META-INF/**"/>
            <exclude path="**"/>
        </filter>
    </artifact>
</overrides>

Most command-line commands accept the --overrides argument to specify this file.

Overrides file syntax

The overrides file must be a valid XML file, with a root named overrides or maven-overrides (the root name is ignored in fact), and containing any of the following elements:

Artifact coordinates or module names

Every element that works on modules accepts the following XML attributes:

  • groupId: for Maven artifact/group pairs (the artifactId is also required then)
  • artifactId: for Maven artifact/group pairs (the groupId is also required then)
  • module: for Ceylon module names
  • version: an optional artifact/module version which will match every version if missing

Defining constants

You define constants with the define element:

<define name="version" value="2.0.10"/>

And you use them in any subsequent XML attribute with the ${constantName} syntax:

<remove module="com.foo.bar" version="${version}"/>

Removing a module entirely

You can remove a module entirely from every import:

<remove module="com.foo.bar"/>

This element accepts the common Artifact coordinates or module names attributes.

Overriding a module version globally

You can replace every import of a given module to use a specific version:

<set module="com.foo.bar" version="2"/>

This element accepts the common Artifact coordinates or module names attributes.

Replacing a module globally

You can replace every import of a given module to use another module:

<replace module="com.foo.bar" version="2">
    <with module="com.foo.gee" version="3"/>
</replace>

These elements accept the common Artifact coordinates or module names attributes.

Overriding a single module's dependencies

You can add/replace/remove or share dependencies of a single module:

<module module="com.foo.bar" version="2">
    <!-- this will add or replace existing dependencies -->
    <add module="com.foo.gee" version="3"/>
    <remove module="com.foo.baz"/>
    <share module="com.foo.dep"/>
</module>

Or for Maven artifacts:

<artifact groupId="com.foo" artifactId="bar" version="2">
    <!-- this will add or replace existing dependencies -->
    <add groupId="com.foo" artifactId="gee" version="3"/>
    <remove groupId="com.foo" artifactId="baz"/>
    <share groupId="com.foo" artifactId="dep"/>
</artifact>

These elements accept the common Artifact coordinates or module names attributes.

Filtering a single module's contents

Sometimes modules include classes you do not want to be seen at runtime, in the metamodel for example, as they are debug/test classes with missing dependencies. You can also exclude them with this:

<artifact groupId="org.osgi" artifactId="org.osgi.core" version="4.0.0">
    <!-- Only include org/osgi/** and META-INF/** -->
    <filter>
        <!-- You can include or exclude and the matching is in sequence and stops at the first match -->
        <include path="org/osgi/**"/>
        <include path="META-INF/**"/>
        <exclude path="**"/>
    </filter>
</artifact>

Example (Google Guice)

Here's an overrides.xml file that lets you import Guice from Maven:

<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
    <module groupId="com.google.inject"
         artifactId="guice"
            version="4.0">
        <share groupId="javax.inject"
            artifactId="javax.inject"/>
    </module>
</overrides>

You can now import Guice like this:

native("jvm")
module com.my.app "1.0.0" {
    import "com.google.inject:guice" "4.0";
}

Note that you don't need this overrides.xml file at all if you use the --auto-export-maven-dependencies flag which is supported by the command line tools. You can also find this setting on the Ceylon Build > Module Repositories page of the Project > Properties for your Ceylon project.

Example (Hibernate JPA solution 1)

Here's an overrides.xml file that lets you import Hibernate's JPA-compliant API from Maven:

<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
    <module groupId="org.hibernate" 
         artifactId="hibernate-entitymanager">
        <share groupId="org.hibernate" 
            artifactId="hibernate-core"/>
        <share groupId="org.javassist" 
            artifactId="javassist"/>
        <share groupId="org.hibernate.javax.persistence" 
            artifactId="hibernate-jpa-2.1-api"/>
    </module>
    <module groupId="org.hibernate" 
         artifactId="hibernate-core">
        <add groupId="javax.transaction" 
          artifactId="jta" 
             version="1.1"
              shared="true"/>
    </module>
</overrides>

Now you can import Hibernate JPA like this:

native("jvm")
module com.my.app "1.0.0" {
    import "org.hibernate:hibernate-entitymanager" "5.0.4.Final";
    import "org.hsqldb:hsqldb" "2.3.1";
}

And define a persistence unit by placing this XML configuration in resources/com/my/module/ROOT/META-INF/persistence.xml where resources is your Ceylon resources directory:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
           version="2.0">

    <persistence-unit name="sample">
        <class>com.my.app.Person</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" 
                     value="org.hsqldb.jdbcDriver"/>
            <property name="javax.persistence.jdbc.url" 
                     value="jdbc:hsqldb:mem:testdb"/>
            <property name="javax.persistence.jdbc.user" 
                     value="sa"/>
            <property name="javax.persistence.jdbc.password" 
                     value=""/>
            <property name="hibernate.dialect" 
                     value="org.hibernate.dialect.HSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" 
                     value="update"/>
        </properties>
    </persistence-unit>

</persistence>

Example (Hibernate JPA solution 2)

Alternatively, instead of using <share/> in overrides.xml, we can do some of the work with the --auto-export-maven-dependencies flag which is supported by the command line tools. You can also find this setting on the Ceylon Build > Module Repositories page of the Project > Properties for your Ceylon project.

With this flag enabled, we can use the following simplified overrides.xml file:

<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
    <module groupId="org.hibernate" 
         artifactId="hibernate-core">
        <add groupId="javax.transaction" 
          artifactId="jta" 
             version="1.1"
              shared="true"/>
    </module>
</overrides>

However, with this solution, we must explicitly import the JPA API module, since the --auto-export-maven-dependencies flag only affects transitive dependencies.

native("jvm")
module com.my.app "1.0.0" {
    import "org.hibernate:hibernate-entitymanager" "5.0.4.Final";
    import "org.hibernate.javax.persistence:hibernate-jpa-2.1-api" "1.0.0.Final";
    import "org.hsqldb:hsqldb" "2.3.1";
}

Example (Spark Framework)

Spark depends on Jetty.

This overrides.xml file allows Spark to be used from Maven:

<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">

    <module groupId="org.eclipse.jetty" 
         artifactId="jetty-server"
            version="9.3.2.v20150730">
        <share groupId="org.eclipse.jetty" 
          artifactId="jetty-io" 
             version="9.3.2.v20150730"/>
    </module>

    <module groupId="org.eclipse.jetty" 
         artifactId="jetty-io"
            version="9.3.2.v20150730">
        <share groupId="org.eclipse.jetty" 
          artifactId="jetty-util" 
             version="9.3.2.v20150730"/>
    </module>

    <module groupId="org.eclipse.jetty" 
         artifactId="jetty-util"
            version="9.3.2.v20150730">
        <share groupId="javax.servlet" 
            artifactId="javax.servlet-api"
               version="3.1.0"/>
    </module>

</overrides>

Now we can import Spark like this:

native("jvm") 
module sparky "1.0.0" {
    import "com.sparkjava:spark-core" "2.3";
}

Alternatively, if we use the --auto-export-maven-dependencies flag, we don't need an overrides.xml file at all. That's probably a more robust solution in this case, given that Jetty comprises a number of internal modules with inter-dependencies that are not all captured in the Maven metadata.