0

I want to write a piece of Java code which can be executed with 2 different kinds of dependencies (or version of a dependency). Namely speaking about org.apache.poi. The code must run on a system with version=2 as well as version=3 or org.apache.poi.

Unfortunately between the versions 2 & 3 some interfaces have changed, code must be build slightly different and there is no way to upgrade both system to the same org.apache.poi version.

So my questions are:

  • Is there a way to compile the code with both versions to not run into compiler errors?
  • Is there a way to execute the right code based on the available org.apache.poi version?
  • What would be an appropriate approach to solve this issue?

As an amendment:
I'm building a code which shall work for two applications which provides an interface in different versions (maven scope of the dependency is provided).
If I have both dependencies in maven, it takes any of the dependencies and IF clauses will fail to compile as Cell.CELL_TYPE_STRING or CellType.STRING is not available in the chosen dependency.
And I would like to have the code working regardless of which dependency is plugged in the application.

            // working with old poi interface
            if (cell != null && cell.getCellType() == Cell.CELL_TYPE_STRING
                    && cell.getRichStringCellValue().getString().trim().equals(cellContent)) {
                return row;
            }
            
            // working with new poi interface
            if (cell != null && cell.getCellType() == CellType.STRING
                    && cell.getRichStringCellValue().getString().trim().equals(cellContent)) {
                return row;
            }
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • Does this answer your question? [Java, Classpath, Classloading => Multiple Versions of the same jar/project](https://stackoverflow.com/questions/6105124/java-classpath-classloading-multiple-versions-of-the-same-jar-project) – Milgo Sep 29 '20 at 10:57
  • So those dependencies are of "provided" scope – Antoniossss Sep 29 '20 at 10:59
  • @Antoniossss no, they aren't. As it is the same dependency with just different versions, how could be ensured that it is going to be built with the correct version? – Maximilian Voss Sep 29 '20 at 11:46
  • @Milgo this doesn't answer the question as it covers how different dependencies can be loaded, but not based on loaded dependency which code has to be executed. – Maximilian Voss Sep 29 '20 at 11:47
  • "As it is the same dependency with just different versions," Either I dont understand you or you dont follow my point. Either YOU are shipping the dependencies along your code, or dependencies are expected to already be in place on the classpath of your code - so they are "provided" by the host. This is what I ment. Now, since that is clear I hope - which is your scenario? You ship apache poi with your piece of code, or it is expected to be already "there" – Antoniossss Sep 29 '20 at 11:50
  • And another thing - do you have in mind having 2 versions on the classpath at once? Or 1 version in separate runtime, and second on another - so its env dependent. – Antoniossss Sep 29 '20 at 11:52
  • @Antoniossss my apologies for misunderstanding you. The dependency is going to be provided with the application where the code is going to be deployed. So yes, the dependency would have the scope=provided. – Maximilian Voss Sep 29 '20 at 12:44

2 Answers2

2

This i probably opinion based, but it seams legit.

First, you will have to create common interface that you will use to do your job.

Second, you will have to create adapter classes that implements that interface and will do required job using particular version of POI library

Third, you will write adapter factory that will return proper instance of adapter. Adapter itself should provide "isSupported" method that will detect if given adapter can be used based on what kind of POI classes are currently loaded (detect by reflection - there must be some version specific classes or other markers)

Then you will put each adapter into separate maven module, so each module can be compiled independently (thus you will have no class conflicts). Each module will have POI dependency in "provided" scope in version that this adapter is going to support

Either module registers itself with the factory in your main module, or factory itself detects all adapters that are available (like @ComponentScan in Spring).

Then you will pack everything into single app bundle. Main module will use only common interface. All in all it will be kind of extensible plugin system

Antoniossss
  • 31,590
  • 6
  • 57
  • 99
2

I do not think there is a single "best way".

Nonetheless, we faced a similar issue in a few of our apps that share a common library. I ended up with a variant of @Antoniossss's variant, except that the library itself does not use dependency injection (the parent app may or may not, but the library is free of it).

To be more specific, and due to transitive dependencies, some of our apps need a certain version of Apache Lucene (e.g. 7.x.y, or more) and other are stuck on older versions (5.5.x).

So we needed a way to build one of our lib against those versions, using maven in our case.

What we ended uses the following principles :

  • We share some code, which is common between all versions of Lucene
  • We have specific code, for each target version of Lucene that has an incompatible API (e.g. package change, non existing methods, ...)
  • We build as many jars as there are supported versions of lucene, with a naming scheme such as groupId:artifact-luceneVersion:version
  • Where the lib is used, direct access to the Lucene API is replaced by access to our specific classes

For exemple, un Lucene v5 there is a org.apache.lucene.analysis.synonym.SynonymFilterFactory facility. In v7 the same functionnality is implemented using org.apache.lucene.analysis.synonym.SynonymGraphFilterFactory e.g. same package, but different class.

What we end up with is providing a com.mycompany.SynonymFilterFactoryAdapter. In the v5 JAR, this class extends the Lucene v5 class, and respectively with v7 or any other version.

In the final app, we always instantiate the com.mycompany object, that behaves just the same as the native org.apache class.

Project structure

The build system being maven, we build it as follow

project root
|- pom.xml
|-- shared
|---|- src/main/java
|---|- src/test/java
|-- v5
|---|- pom.xml
|-- v7
|---|- pom.xml

Root pom

The root pom is a classic multimodule pom, but it does not declare the shared folder (notice that the shared folder has no pom).

<modules>
    <module>v5</module>
    <module>v7</module>
</modules>

The shared folder

The shared folder stores all non-version specific code and the tests. On top of that, when a version specific class is needed, it does not code against the API of this class (e.g. it does not import org.apache.VersionSpecificStuff), it does against com.mycompany.VersionSpecificStuffAdapter).

The implementation of this Adapter being left to the version specific folders.

Version specific folders

The v5 folder declares in its artifact id the Lucene version it compiles to, and of course declares it as a dependency

....
<artifactId>myartifact-lucene-5.5.0</artifactId>
....
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-common</artifactId>
    <version>5.5.0</version>
</dependency>

But the real "trick" is that it declares an external source folder for classes and tests using the build-helper-maven-plugin : see below how the source code from the shared folder is imported "as if" it was from this project itself.

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>build-helper-maven-plugin</artifactId>
            <version>3.0.0</version>
            <executions>
                <execution>
                    <id>add-5.5.0-src</id>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>add-source</goal>
                    </goals>
                    <configuration>
                        <sources>
                            <source>../shared/src/main/java</source>
                        </sources>
                    </configuration>
                </execution>
                <execution>
                    <id>add-5.5.0-test</id>
                    <phase>generate-test-sources</phase>
                    <goals>
                        <goal>add-test-source</goal>
                    </goals>
                    <configuration>
                        <sources>
                            <source>../shared/src/test/java</source>
                        </sources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

For the whole implementation to work, it provides the Adapter implementations in its own source folder src/main/java, e.g.

package com.mycompany

public class VersionSpecificStuffAdapter extends org.apache.VersionSpecificStuff {
}

If both the v5 and the v7 package do it the same way, then client code using the com.mycompany.xxxAdapter will always compile, and under the hood, get the corresponding implementation of the library.

This is one way to do it. You can also, as already suggested, define your whole new interfaces and have clients of your lib code against your own interface. This is kind of cleaner, but depending on the case, may imply more work.

In your edit, you mention refering to constants that are not defined the same way, e.g. CellType.TYPE_XX.

In the version specific code, you could either produce another constant MyCellType.TYPE_XX that would duplicate the actual constant, under a stable name.

In case of an enum, you could create a CellTypeChecker util with a method isCellTypeXX(cell), that would be implemented in a version specific way.

v7 folder

It's pretty much the same structure, you just swap what changed between v5 and v7.

Caveats

This may not always scale.

If you have hundreds of types you need to adapt, this is cumbersome to say the least.

If you have 2 or more libs you need to cross-compile against (e.g. mylib-poi-1.0-lucene-5.5-guava-19-....) it's a mess.

If you have final classes to adapt, it gets harder.

You have to test to make sure every JAR has all the adapters. I do that by testing each Adapted class in the shared test folder.

GPI
  • 9,088
  • 2
  • 31
  • 38