Is there a way to update modular library without updating main application? - java

Before modular application organization, I had one main JavaFX application that load custom created multiple libraries for different options and possibilities in main app.
In old way of implementation, I just send new library to update, main application reads all libraries from folder and it works like a charm. But in a modular system, if my application wants to use new modular library that I send, it needs to update its module-info file, apropos I need to send updates for modular library and for main application.
Just imagine, it would be like, chrome need to send browser update for every new plugin that is created. As I can see, with Java modularity system you can't create modular applications.
Is there a way import new module without updating main application or some other way around?

Java has a class for that: ServiceLoader.
If we assume you have a “service provider” interface named PluginProvider, other modules can declare themselves to provide that service by putting this in their respective module-info.java descriptors:
provides com.john.myapp.PluginProvider with com.library.MyProvider;
Your application would then state that it uses that service in its own module-info:
uses com.john.myapp.PluginProvider;
And your application’s code would create a ModuleFinder that looks in the directory (or directories) where you expect those plugin modules to reside, then pass that ModuleFinder to a Configuration which can be used to create a ModuleLayer for the ServiceLoader:
public class PluginLoader {
private final ServiceLoader<PluginProvider> loader;
public PluginLoader() {
Path pluginDir = Paths.get(System.getProperty("user.home"),
".local", "share", "MyApplication", "plugins");
ModuleLayer layer = PluginProvider.class.getModule().getLayer();
layer = layer.defineModulesWithOneLoader(
layer.configuration().resolveAndBind(
ModuleFinder.of(),
ModuleFinder.of(pluginDir),
Collections.emptySet()),
PluginProvider.class.getClassLoader());
loader = ServiceLoader.load(layer, PluginProvider.class);
}
public Stream<PluginProvider> getAll() {
return loader.stream();
}
public void reload() {
loader.reload();
}
}
You might even want to watch the plugin directory for new or removed files:
try (WatchService watch = pluginDir.getFileSystem().newWatchService()) {
pluginDir.register(watch,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.OVERFLOW);
WatchKey key;
while ((key = watch.take()).isValid()) {
loader.reload();
key.reset();
}
}

Related

What else am I missing to load service providers using the new Java module system?

I'm adding module-info.javas to Ikonli packages and I'm running into trouble with their service classes. ikonli-core defines an interface called IkonHandler. ikonli-fontawesome5-pack has a service provider for the IkonHandler called FontAwesomeSolidIkonHandler. These service prodivers are used by ikonli-javafx's IkonResolver.
Given this, I created these module definitions:
module org.kordamp.ikonli.core {
exports org.kordamp.ikonli;
}
module org.kordamp.ikonli.javafx {
exports org.kordamp.ikonli.javafx;
uses org.kordamp.ikonli.IkonHandler;
requires javafx.graphics;
requires org.kordamp.ikonli.core;
}
module org.kordamp.ikonli.fontawesome5 {
exports org.kordamp.ikonli.fontawesome5;
provides org.kordamp.ikonli.IkonHandler with org.kordamp.ikonli.fontawesome5.FontAwesomeBrandsIkonHandler, org.kordamp.ikonli.fontawesome5.FontAwesomeRegularIkonHandler, org.kordamp.ikonli.fontawesome5.FontAwesomeSolidIkonHandler;
requires org.kordamp.ikonli.core;
requires org.kordamp.jipsy;
}
They might not be complete, but they are complete enough so that when my application starts, it fails with this error:
java.lang.UnsupportedOperationException: Cannot resolve 'fas-user'
which is throw when no handler managed to load the icon:
public IkonHandler resolveIkonHandler(String value) {
requireNonNull(value, "Ikon description must not be null");
for (IkonHandler handler : HANDLERS) {
if (handler.supports(value)) {
return handler;
}
}
throw new UnsupportedOperationException("Cannot resolve '" + value + "'");
}
The reason why that is happening is that HANDLERS is empty. HANDLERS is loaded at startup by this code:
ClassLoader classLoader = IkonResolver.class.getClassLoader();
ServiceLoader<IkonHandler> loader = ServiceLoader.load(IkonHandler.class, classLoader);
for (IkonHandler handler : loader) {
HANDLERS.add(handler);
handler.setFont(Font.loadFont(classLoader.getResource(handler.getFontResourcePath()).toExternalForm(), 16));
}
but with the module definitions quoted above, ServiceLoader.load(IkonHandler.class, classLoader) finds no service providers.
What am I missing?
What I was missing was requiring fontawesome5 in the module-info.java of my application:
requires org.kordamp.ikonli.fontawesome5;
"My app was not requiring the the fontawesome5 module." Your app doesn't need to require it. In fact, your app shouldn't require it. You're missing the point of uses/provides.
Edit You make a provider available by putting it on the modulepath and watching the module system go to work. Your ikonli.javafx module uses the IkonHandler interface, and your ikonli.fontawesome5 module provides an implementation of the IkonHandler interface. That's all the module system needs to bind them together. It's wrong for ikonli.javafx to require ikonli.fontawesome5. ikonli.fontawesome5 shouldn't even export the package that it's exporting, because that allows anyone who requires ikonli.fontawesome5 to access the provider classes like FontAwesomeBrandsIkonHandler directly.

Using SystemLoader / SpringFactoryLoader to load external Jar in Spring-Project

first: I'm really new to spring-boot and maven. So I still don't get how everything plugs together.
What I'm trying to achieve is some kind of plugin-feature for my application. From my research it seems the best way to do this is using ServiceLoader or the spring-boot implmentation of the SpringFactoriesLoader.
According to several instructions from the web I put two projects together
James (the main application) GitHub
TemperatureSensor (the plugin) GitHub
The JamesApplication provides an interfaces which is supposed to be implemented (de.maxrakete.james.device.domain.DeviceInterface).
The TemperatureSensor implements said class and exposes this in several ways.
For the ServiceLoader in in the file META-INF\services\de.maxrakete.james.device.domain.DeviceInterface with this content
de.maxrakete.james.plugin.TemperatureSensor.TemperatureSensor
For the SpringFactoriesLoader in the file META-INF\spring.factories with this content
de.maxrakete.james.device.domain.DeviceInterface=de.maxrakete.james.plugin.TemperatureSensor.TemperatureSensor
According to this page I tried two different implementations (see in the onApplicationEvent-function) in the MainApplication:
#SpringBootApplication
public class JamesApplication implements ApplicationListener<ApplicationReadyEvent> {
public static void main(String[] args) {
SpringApplication.run(JamesApplication.class, args);
ClassLoader cl = ClassLoader.getSystemClassLoader();
URL[] urls = ((URLClassLoader)cl).getURLs();
for(URL url: urls){
System.out.println("Classpath file: " + url.getFile());
}
}
#Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ServiceLoader<DeviceInterface> loader = ServiceLoader.load(DeviceInterface.class);
loader.iterator();
List<DeviceInterface> foos = SpringFactoriesLoader.loadFactories(DeviceInterface.class, null);
}
}
I'm trying both ways to load the jar, but nothing is happening (I'm supposed to get some log-messages from the plugin) but this is not happening.
The way I'm running the application is like this:
java -cp "./plugins/TemperatureSensor-0.0.1-SNAPSHOT.jar" -jar james.war
As you see I'm trying to add the jar in the subfolder to the classpath, but in the ouput of the main-function (where I try to print all the files in the classpath) I only get Classpath file: /home/max/folder/james.war
Conclusion
So, there are three possible error-sources
Wrong cli command to add classpath files
Wrong declaration of interfaces in the META-INF folder
Wrong implementation of the Loader
Maybe I'm compiling the sources the wrong way?
Wrong configuration of the pom.xml
I really have no idea what the problem might be. I tried to provide you with as much information as possible and all the steps of my research. I hope someone finds some helpful clues, which I might have overlooked.
Thanks veryone!

Strange Gluon project structure for JavaFX - Android porting

The gradle build of Gluon plugin (in Netbeans 8.0.2) for porting JavaFX to Android creates the following directory structures:
Source Packages [Java]
Android/Java Packages
Desktop/Java Packages
Ios/Java Packages
Each of these directories contain java packages inside them. Generally Gluon build would create the "main" class for us in one java package inside "Source Packages" directory [the name Packages with "Source Packages" might be misleading since it is not a Java package, its just a file system directory]. This main class extends Javafx Application class and thus is the entry point into our application.
The Android API is accessible only in Android/Java Packages directory for any java package created inside it. Say, the android.Vibrator class is only referr-able here.
The problem is, we cannot refer to a class created inside any Java package inside Android/Java directory to any java package created inside Source Packages [Java] directory!! If this is the case then how would we take the application forward from the start() method of javafx.Application into say android.Vibrator.
The gluon project structure has a snapshot at: How to reference android.jar in Gluon Project
As you know, JavaFXPorts project allows deploying JavaFX apps in Desktop, Android and iOS devices. When it's just pure JavaFX code, the project is added on the Main scope, and from there it will be visible in all those platforms.
Only in the case you need some platform specific code, you should add it on the corresponding package.
As you have mentioned, by default from the Main package you won't see the added code in a platform package, so you have to provide a way for that.
If you check the HelloPlatform sample on JavaFXPorts repository, you will find a PlatformService class to load the packages using ServiceLoader.
Another possibility is using Class.forName() to load dynamically the classes at runtime, once we know the platform where the app is running.
I suggest you have a look at the Gluon Down project, that manages for you several platform specific services, and provides you with uniform, platform-independent API.
For those services not available yet in Down (feel free to contribute), you can implement them like in this simple app created using Gluon Plugin.
Source Packages [Java]
First, create a getPlatform() method, and add the referred classes to each specific platform. For instance, add org.gluonoss.vibrator.GluonAndroidPlatform.java on Android package.
public class GluonPlatformFactory {
public static GluonPlatform getPlatform() {
try {
String platform = System.getProperty("javafx.platform", "desktop");
String path = "org.gluonoss.vibrator.GluonDesktopPlatform";
if(platform.equals("android")) {
path = "org.gluonoss.vibrator.GluonAndroidPlatform";
} else if(platform.equals("ios")) {
path = "org.gluonoss.vibrator.GluonIosPlatform";
}
return (GluonPlatform) Class.forName(path).newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
System.out.println("Platform Error "+e.getMessage());
}
return null;
}
}
Now, create an interface, with the method you want on all your platforms:
public interface GluonPlatform {
void vibrate();
}
Finally, on your main class retrieve the platform and call your method:
#Override
public void start(Stage stage) {
final Button button = new Button("Click me!");
button.setOnAction(e-> GluonPlatformFactory.getPlatform().vibrate());
StackPane root = new StackPane(button);
Rectangle2D visualBounds = Screen.getPrimary().getVisualBounds();
Scene scene = new Scene(root, visualBounds.getWidth(), visualBounds.getHeight());
stage.setScene(scene);
stage.show();
}
Desktop/Java Packages
Add the vibrate method. For now leave it empty, but you could add a Timeline to move the button, for instance.
public class GluonDesktopPlatform implements GluonPlatform {
#Override
public void vibrate() {
System.out.println("Vibrating!");
}
}
Android/Java Packages
Add the vibrate method. Notice that we have to use FXActivity, which is the bridge between the JavaFX thread and the Android activity.
public class GluonAndroidPlatform implements GluonPlatform {
#Override
public void vibrate() {
Vibrator v = (Vibrator) FXActivity.getInstance().getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(500);
}
}
Don't forget to add the required permission on your AndroidManifest file (you will find it under src/android/AndroidManifest.xml.
Now you can deploy the project and run it on Desktop (gradlew run) and it will work, and install it on Android (gradlew androidInstall), and it will work too.

How to keep a jar file external but still use its classes in my Android project?

I need to have a jar file located in a main/assets directory within an Android project. It is important the jar file is located there.
With my main Android project is there a way to reference this jar file in my code and to use its classes?
To be clear I don't want to add the jar to the main project once compiled.
EDIT: I have tried the link below and it seems to load the Class file I've stated. But I'm strugging how to define constructor arguments for the dynamically loaded Class.
android-custom-class-loading-sample
EDIT2
Nearly there. I've confirmed the class is loaded from my classes.jar. I'm stuck instantiating it though.
On the licenseValidatorClazz.getConstructor line I get the error below. I'm guessing I'm missing something from my Interface file?
java.lang.NoSuchMethodException: [interface com.google.android.vending.licensing.Policy, interface com.google.android.vending.licensing.DeviceLimiter, interface com.google.android.vending.licensing.LicenseCheckerCallback, int, class java.lang.String, class java.lang.String]
public Class licenseValidatorClazz = null;
public LicenseValidator validator;
...
// Initialize the class loader with the secondary dex file.
DexClassLoader cl = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(),
optimizedDexOutputPath.getAbsolutePath(),
null,
mContext.getClassLoader());
try {
// Load the library class from the class loader.
licenseValidatorClazz = cl.loadClass("com.google.android.vending.licensing.LicenseValidator");
validator = (LicenseValidator) licenseValidatorClazz.getConstructor(Policy.class,DeviceLimiter.class,LicenseCheckerCallback.class,int.class,String.class,String.class).newInstance(ddd, new NullDeviceLimiter(),
callback, generateNonce(), mPackageName, mVersionCode);
} catch (Exception exception) {
// Handle exception gracefully here.
exception.printStackTrace();
}
I have an Interface which contains the functions to pass to the loaded class.
public interface LicenseValidator
{
public LicenseCheckerCallback getCallback();
public int getNonce();
public String getPackageName();
public void verify(PublicKey publicKey, int responseCode, String signedData, String signature);
public void handleResponse(int response, ResponseData rawData);
public void handleApplicationError(int code);
public void handleInvalidResponse();
}
TO use an external jar to be associated with your application and use it during runtime, it needs to be in dalvik format since normal jars cannot work under dalvikVM.
Convert your files using the dx tool
using aapt cmd , add those classes.dex to your jar file.
Now this jar which contains files in dalvik format can be loaded into our project.
Here is a post which explains the procedure to accomplish it.
There are steps to accomplish this.
You have to make a copy of your JAR file into the private internal storage of your aplication.
Using the dx tool inside the android folder, you have to generate a classes.dex file associated with the JAR file. The dx tool will be at the location /android-sdks/build-tools/19.0.1 (this file is needed by the Dalvik VM, simply jar can not be read by the dalvik VM))
Using the aapt tool command which is also inside the same location, you have to add the classes.dex to the JAR file.
This JAR file could be loaded dynamically using DexClassLoader.
If you are making a JAR from any one your own library, you have to do this steps (1-4) every time when there is a change in your library source code. So you can automate this steps by creating a shell script(in Mac/Linux/Ubuntu) or batch scripts(in Windows). You can refere this link to understand how to write shell scripts.
Note : One situation for implementing this method is, when it is impossible to add the JAR files directly to the build path of core project and need to be loaded dynamically at run time. In normal cases the JAR files could be added to the build path.
please check this link for the detailed code and implementation.
How to load a jar file at runtime
Android: How to dynamically load classes from a JAR file?
Hope this helps!!
You should try out the Services API - java.util.ServiceLoader
You define a service interface and its implementations in your jar.
package com.my.project;
public interface MyService { ... }
public class MyServiceBarImpl implements MyService { ... }
public class MyServiceFooImpl implements MyService { ... }
Then you define the services contained within the jar file in the META-INF/services/ directory. For instance, in the file 'META-INF/services/com.my.project.MyService', you list the provider classes.
# Known MyService providers.
com.my.project.MyServiceBarImpl # The original implementation for handling "bar"s.
com.my.project.MyServiceFooImpl # A later implementation for "foo"s.
Then, in your main codebase, you can instantiate a MyService instance with the ServiceLoader:
for (MyService service : ServiceLoader.load(MyService.class)) {
//Perform some test to determine which is the right MyServiceImpl
//and then do something with the MyService instance
}
These examples are taken more-or-less straight from the API, although I've changed the package names to make them slightly less annoying to read.

Best practice: Extending or overriding an Android library project class

We're using an Android Library Project to share core classes and resources across different builds (targets) of our Android application. The Android projects for each specific target reference the Core library project (behind the scenes, Eclipse creates and references a jar from the referenced library project).
Overriding resources such as images and XML layouts is easy. Resource files placed in the target project, such as the app icon or an XML layout, automatically override the core library's resources with the same name when the app is built. However, sometimes a class needs to be overridden to enable target-specific behavior. For example, the Amazon target preferences screen cannot contain a link to the Google Play app page, requiring a change in the Amazon project's preferences.xml and preferences Activity class.
The goal is to reduce the amount of duplicate code among target projects while removing as much target-specific code from the Core library as possible. We've come up with a couple of approaches to implement logic specific to different targets:
Write the target-specific functions within Core library classes and use if/switch blocks to select behavior based on product SKU. This approach is not very modular and bloats the Core library codebase.
Extend the particular Core class in a target project and override the base (Core) class functions as needed. Then keep a reference to the base-class object in the Core library and instantiate it with an extended class object (from How to override a class within an Android library project?)
Are there other strategies to override or extend an Android library project class? What are some of the best practices for sharing and extending common classes among Android app targets?
Library project is referenced as a raw project dependency (source-based mechanism), not as a compiled jar dependency (compiled-code based library mechanism).
#yorkw this is not true for the latest versions of ADT Plugin for Eclipse
http://developer.android.com/sdk/eclipse-adt.html
From version 17 Change log
New build features
Added feature to automatically setup JAR dependencies. Any .jar files in the /libs folder are added to the build configuration (similar to how the Ant build system works). Also, .jar files needed by library projects are also automatically added to projects that depend on those library projects. (more info)
More info http://tools.android.com/recent/dealingwithdependenciesinandroidprojects
Before that, update overwriting of the Activity from Library project was easy, just exclude the class. Now the library is included as jar file, and there is no way to exclude class file from jar dependency.
EDIT:
My solution to overwrete/extend Activity from library jar:
I created a simple util class:
public class ActivityUtil {
private static Class getActivityClass(Class clazz) {
// Check for extended activity
String extClassName = clazz.getName() + "Extended";
try {
Class extClass = Class.forName(extClassName);
return extClass;
} catch (ClassNotFoundException e) {
e.printStackTrace();
// Extended class is not found return base
return clazz;
}
}
public static Intent createIntent(Context context, Class clazz) {
Class activityClass = getActivityClass(clazz);
return new Intent(context, activityClass);
}
}
In order to overwrite a library's "SampleActivity" class it a the project which depends on that library, create a new class with the name SampleActivityExtended in the project in the same package and add the new activity to your AndroidManifest.xml.
IMPORTANT: all intents referencing overwritten activities should be created through the util class in the following manner:
Intent intent = ActivityUtil.createIntent(MainActivity.this, SampleActivity.class);
...
startActivity(intent);
behind the scenes, Eclipse creates and references a jar from the referenced library project.
This is not quite accurate. Library project is referenced as a raw project dependency (source-based mechanism), not as a compiled jar dependency (compiled-code based library mechanism). Currently Android SDK does not support exporting a library project to a self-contained JAR file. The library project must always be compiled/built indirectly, by referencing the library in the dependent application and building that application. When build dependent project, the compiled source and raw resources that need to be filtered/merged from Library project are copied and properly included in the final apk file. Note that Android team had started revamping the whole Library Project design (move it from ource-based mechanism to compiled-code based library mechanism) since r14, as mentioned in this earlier blog post.
What are some of the best practices for sharing and extending common classes among Android app targets?
The solution given by Android is Library Project.
The solution given by Java is Inheritance and Polymorphism.
Come together, the best practice IMO is the second option you mentioned in the question:
2.Extend the particular Core class in a target project and override the base (Core) class functions as needed. Then keep a reference to the base-class object in the Core library and instantiate it with an extended class object (from Android library project - How to overwrite a class?)
From my personal experience, I always use Android Library Project (Sometimes with Regular Java Project, for implementing/building common-lib.jar that contains only POJO) manage common code, for instance SuperActivity or SuperService, and extends/implements proper classes/interfaces in the dependent project for Polymorphism.
Solution based on PoisoneR's solution and Turbo's solution.
public static Class<?> getExtendedClass(Context context, String clsName) {
// Check for extended activity
String pkgName = context.getPackageName();
Logger.log("pkgName", pkgName);
String extClassName = pkgName + "." + clsName + "Extended";
Logger.log("extClassName", extClassName);
try {
Class<?> extClass = Class.forName(extClassName);
return extClass;
} catch (ClassNotFoundException e) {
e.printStackTrace();
// Extended class is not found return base
return null;
}
}
The benefits of this is that
The extended class can be in the project's package, not the library's package. Thanks to Turbo for this part.
By taking a String as an argument instead of a Class object, this method is able to be used even with ProGuard. getName() is where the problem is with ProGuard, as that will return something like "a" instead of the name of the original class. So in the original solution instead of looking for ClassExtended it will look for aExtended instead, something which does not exist.
What about using a callback approach here? (Okay, callback is a little bit misleading but I currently have no other word for it:
You could declare an interface in every Activity which should/may be expanded by the user. This interface will have methods like List<Preference> getPreferences(Activity activity) (pass whatever parameters you need here, I would use an Activity or at least a Context to be futureproof).
This approach could give you what you want when I have understood it correctly. While I haven't done this before and don't know how other people handle this I would give it a try and see if it works.
Could you, please, clarify what is different in Kindle and regular Android?
I think - they are the same.
What you need is different resources for Kindle and other devices. Then use appropriate resource.
For example I use 2 links to store:
<string name="appStore"><a href=http://market.android.com/details?id=com.puzzle.jigsaw>Android Market</a> or <a href=http://www.amazon.com/gp/mas/dl/android?p=com.puzzle.jigsaw>Amazon Appstore</a> <br>http://market.android.com/details?id=com.puzzle.jigsaw <br>href=http://www.amazon.com/gp/mas/dl/android?p=com.puzzle.jigsaw</string>
<string name="appStore_amazon"><a href=http://www.amazon.com/gp/mas/dl/android?p=com.puzzle.jigsaw>Amazon Appstore</a> <br>href=http://www.amazon.com/gp/mas/dl/android?p=com.puzzle.jigsaw</string>
and use appStore for all none Amazone product and appStore_amazon for Kindle.
How to determine where are you on run time - that would be another question which was answered here many times.
I was inspired by PoinsoneR's answer to create a Utility class to do the same thing for Fragments - override a fragment in an android Library. The steps are similar to his answer so I won't go into great detail, but here is the class:
package com.mysweetapp.utilities;
import android.support.v4.app.Fragment;
public class FragmentUtilities
{
private static Class getFragmentClass(Class clazz)
{
// Check for extended fragment
String extClassName = clazz.getName() + "Extended";
try
{
Class extClass = Class.forName(extClassName);
return extClass;
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
// Extended class is not found return base
return clazz;
}
}
public static Fragment getFragment(Class clazz)
{
Class fragmentClass = getFragmentClass(clazz);
Fragment toRet = null;
try
{
toRet = (Fragment)fragmentClass.newInstance();
return toRet;
}
catch (InstantiationException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (IllegalAccessException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
return toRet;
}
}
Usage:
FragmentUtilities.getFragment(MySpecialFragment.class)
You can also use an Activity factory if you need to provide extended activitys for differnt build variants and have your library deal with the abstract factory alone. This can be set in your build variants Application file.

Categories

Resources