1. Project

1.1. Building

The JMinor framework is built with Gradle and includes the Gradle Wrapper, so assuming you have cloned the repository and worked your way into the project directory you can build the framework by running the following command.

gradlew build
Note
This may take a few minutes, depending on the machine.

To install the JMinor framework into your local Maven repository run the following command.

gradlew publishToMavenLocal

1.2. Running the demos

Note
The demos use an embedded in-memory database, so changes to data do not persist.

1.2.1. Local database connection

You can start by running a client from one of the demo projects (empdept, chinook, petstore or world) with a local database connection.

gradlew :jminor-demos-chinook:runClientLocal

1.2.2. Remote database connection

In order to run a client with a remote connection we must first start the remote server.

gradlew :jminor-framework-server:runServer

To run a demo client with a remote connection use the following command.

gradlew :jminor-demos-chinook:runClientRMI

You can run the Server Monitor application to see how the server is behaving, with the following command.

gradlew :jminor-swing-framework-server-monitor:runServerMonitor
Note
The client handles server restarts gracefully, you can try shutting down the server via the Server Monitor, play around in the client until you get a 'Connection refused' exception. After you restart the server the client simply reconnects and behaves as if nothing happened.

1.3. Code quality

2. Architecture

The JMinor framework is based on a three tiered architecture.

  • Database layer

  • Model layer

  • UI layer

2.1. Database layer

The EntityConnection class defines the database layer. See Manual:EntityConnection

2.2. Model layer

The EntityModel class defines the model layer. See Manual:EntityModel.

2.3. UI layer

The EntityPanel class defines the UI layer. See Manual:EntityPanel.

3. Client

3.1. Features

  • Lightweight single threaded client with a simple synchronous event model

  • Provides a practically mouse free user experience

  • Graceful handling of network outages and server restarts

  • Clear separation between model and UI

  • Easy to use load testing harness provided for application models

  • UI data bindings for most common components provided by the framework

  • Implementing data bindings for new components is made simple with building blocks provided by the framework.

  • The default UI layout is a simple and intuitive “waterfall” master-detail view

  • Extensive searching and filtering capabilities

  • Flexible keyboard-centric UI based on tab and split panes, detachable panels and toolbars

  • Detailed logging of client actions

3.2. Default client layout

The default master/detail panel layout.

Client UI

3.3. Architecture

3.3.1. UI

ui architecture

3.3.2. Model

model architecture

3.3.3. Assembly

EntityModel
  /**
   * Creates a SwingEntityModel based on the Chinook.T_ARTIST entity
   * with a detail model based on Chinook.T_ALBUM
   * @param connectionProvider the connection provider
   */
  static SwingEntityModel artistModel(EntityConnectionProvider connectionProvider) {
    //initialize a default edit model
    SwingEntityEditModel artistEditModel =
            new SwingEntityEditModel(Chinook.T_ARTIST, connectionProvider);
    //initialize a default table model
    SwingEntityTableModel artistTableModel =
            new SwingEntityTableModel(Chinook.T_ARTIST, connectionProvider);
    //initialize a default model using the edit and table models
    SwingEntityModel artistModel =
            new SwingEntityModel(artistEditModel, artistTableModel);

    //Note that this does the same as the above, that is, initializes
    //a SwingEntityModel with a default edit and table model
    SwingEntityModel albumModel =
            new SwingEntityModel(Chinook.T_ALBUM, connectionProvider);

    artistModel.addDetailModel(albumModel);

    return artistModel;
  }
EntityPanel
  /**
   * Creates a EntityPanel based on the Chinook.T_ARTIST entity
   * with a detail panel based on Chinook.T_ALBUM
   * @param connectionProvider the connection provider
   */
  static EntityPanel artistPanel(EntityConnectionProvider connectionProvider) {
    //initialize the EntityModel to base the panel on
    SwingEntityModel artistModel = artistModel(connectionProvider);
    //fetch the edit model
    SwingEntityEditModel artistEditModel = artistModel.getEditModel();
    //fetch the table model
    SwingEntityTableModel artistTableModel = artistModel.getTableModel();
    //fetch the album detail model
    SwingEntityModel albumModel = artistModel.getDetailModel(Chinook.T_ALBUM);

    //create a EntityEditPanel instance, based on the artist edit model
    EntityEditPanel artistEditPanel = new EntityEditPanel(artistEditModel) {
      @Override
      protected void initializeUI() {
        createTextField(Chinook.ARTIST_NAME).setColumns(15);
        addPropertyPanel(Chinook.ARTIST_NAME);
      }
    };
    //create a EntityTablePanel instance, based on the artist table model
    EntityTablePanel artistTablePanel = new EntityTablePanel(artistTableModel);
    //create a EntityPanel instance, based on the artist model and
    //the edit and table panels from above
    EntityPanel artistPanel = new EntityPanel(artistModel, artistEditPanel, artistTablePanel);

    //create a new EntityPanel, without an edit panel and
    //with a default EntityTablePanel
    EntityPanel albumPanel = new EntityPanel(albumModel);

    artistPanel.addDetailPanel(albumPanel);

    return artistPanel;
  }

3.3.4. Full Example

Show code
package org.jminor.framework.demos.chinook.tutorial;

import org.jminor.common.db.Database;
import org.jminor.common.db.Databases;
import org.jminor.common.user.Users;
import org.jminor.framework.db.EntityConnectionProvider;
import org.jminor.framework.db.local.LocalEntityConnectionProvider;
import org.jminor.framework.demos.chinook.domain.Chinook;
import org.jminor.framework.demos.chinook.domain.impl.ChinookImpl;
import org.jminor.swing.framework.model.SwingEntityEditModel;
import org.jminor.swing.framework.model.SwingEntityModel;
import org.jminor.swing.framework.model.SwingEntityTableModel;
import org.jminor.swing.framework.ui.EntityEditPanel;
import org.jminor.swing.framework.ui.EntityPanel;
import org.jminor.swing.framework.ui.EntityTablePanel;

/**
 * When running this make sure the chinook demo module directory is the
 * working directory, due to a relative path to a db init script
 */
public final class ClientArchitecture {

  // tag::entityModel[]
  /**
   * Creates a SwingEntityModel based on the Chinook.T_ARTIST entity
   * with a detail model based on Chinook.T_ALBUM
   * @param connectionProvider the connection provider
   */
  static SwingEntityModel artistModel(EntityConnectionProvider connectionProvider) {
    //initialize a default edit model
    SwingEntityEditModel artistEditModel =
            new SwingEntityEditModel(Chinook.T_ARTIST, connectionProvider);
    //initialize a default table model
    SwingEntityTableModel artistTableModel =
            new SwingEntityTableModel(Chinook.T_ARTIST, connectionProvider);
    //initialize a default model using the edit and table models
    SwingEntityModel artistModel =
            new SwingEntityModel(artistEditModel, artistTableModel);

    //Note that this does the same as the above, that is, initializes
    //a SwingEntityModel with a default edit and table model
    SwingEntityModel albumModel =
            new SwingEntityModel(Chinook.T_ALBUM, connectionProvider);

    artistModel.addDetailModel(albumModel);

    return artistModel;
  }
  // end::entityModel[]
  // tag::entityPanel[]
  /**
   * Creates a EntityPanel based on the Chinook.T_ARTIST entity
   * with a detail panel based on Chinook.T_ALBUM
   * @param connectionProvider the connection provider
   */
  static EntityPanel artistPanel(EntityConnectionProvider connectionProvider) {
    //initialize the EntityModel to base the panel on
    SwingEntityModel artistModel = artistModel(connectionProvider);
    //fetch the edit model
    SwingEntityEditModel artistEditModel = artistModel.getEditModel();
    //fetch the table model
    SwingEntityTableModel artistTableModel = artistModel.getTableModel();
    //fetch the album detail model
    SwingEntityModel albumModel = artistModel.getDetailModel(Chinook.T_ALBUM);

    //create a EntityEditPanel instance, based on the artist edit model
    EntityEditPanel artistEditPanel = new EntityEditPanel(artistEditModel) {
      @Override
      protected void initializeUI() {
        createTextField(Chinook.ARTIST_NAME).setColumns(15);
        addPropertyPanel(Chinook.ARTIST_NAME);
      }
    };
    //create a EntityTablePanel instance, based on the artist table model
    EntityTablePanel artistTablePanel = new EntityTablePanel(artistTableModel);
    //create a EntityPanel instance, based on the artist model and
    //the edit and table panels from above
    EntityPanel artistPanel = new EntityPanel(artistModel, artistEditPanel, artistTablePanel);

    //create a new EntityPanel, without an edit panel and
    //with a default EntityTablePanel
    EntityPanel albumPanel = new EntityPanel(albumModel);

    artistPanel.addDetailPanel(albumPanel);

    return artistPanel;
  }
  // end::entityPanel[]

  public static void main(final String[] args) {
    // Configure the database
    Database.DATABASE_TYPE.set(Database.Type.H2.toString());
    Database.DATABASE_EMBEDDED_IN_MEMORY.set(true);
    Database.DATABASE_INIT_SCRIPT.set("src/main/sql/create_schema.sql");
    //initialize a connection provider, this class is responsible
    //for supplying a valid connection or throwing an exception
    //in case a connection can not be established
    EntityConnectionProvider connectionProvider =
            new LocalEntityConnectionProvider(Databases.getInstance())
                    .setDomainClassName(ChinookImpl.class.getName())
                    .setUser(Users.parseUser("scott:tiger"));

    final EntityPanel artistPanel = artistPanel(connectionProvider);
    //lazy initialization of the UI
    artistPanel.initializePanel();
    //fetch data from the database
    artistPanel.getModel().refresh();

    //uncomment the below line to display the panel
//    displayInDialog(null, artistPanel, "Artists");

    connectionProvider.disconnect();
  }
}

3.4. Configuration

3.4.1. Example configuration file

jminor.client.connectionType=local
jminor.db.embeddedInMemory=true
jminor.db.initScript=classpath:create_schema.sql
jminor.db.host=h2db/h2
jminor.db.type=h2
jminor.report.path=reports

3.5. Usage

3.5.1. Keyboard shortcuts

Navigation
  • Ctrl+Alt+ Up

  • Ctrl+Alt+ Down

  • Ctrl+Alt+ Left

  • Ctrl+Alt+ Right

Resizing
  • Shift+Alt+ Left

  • Shift+Alt+ Right

  • Ctrl+Alt+↑/↓ Toggle edit panel

Transfer focus
  • Ctrl+E Edit panel (initial focus component)

  • Ctrl+T Table

  • Ctrl+I Input field

  • Ctrl+S Search field

  • Ctrl+F Quick search

Edit panel
  • Alt+S Save

  • Alt+A Add (when available)

  • Alt+U Update

  • Alt+D Delete

  • Alt+C Clear

  • Alt+R Refresh

Table panel
  • Del Delete selected

  • Shift+Alt+ Move column left

  • Shift+Alt+ Move column right

  • Alt+ Resize column left

  • Alt+ Resize column right

4. Server

4.1. Features

  • Firewall friendly; uses one way communications without callbacks and can be configured to serve on a single fixed port

  • Integrated web server for serving Web Start applications and files, based on Jetty

  • All user authentication left to the database by default

  • Comprehensive administration and monitoring facilities via the ServerMonitor

  • Featherweight server with moderate memory and CPU usage

4.2. Security

4.2.1. Authentication

The JMinor server does not perform any user authentication by default, it leaves that up the the underlying database. An authentication layer can be added by implementing a LoginProxy and setting the following system property.

jminor.server.loginProxyClasses=com.app.server.AppLoginProxy # (1)
  1. A comma separated list of login proxies

Login proxy examples

4.2.2. SSL encryption

To enable SSL encryption between client and server, create a keystore and truststore pair and set the following system properties.

Server side
jminor.server.connection.sslEnabled=true # (1)
javax.net.ssl.keyStore=keystore.jks
javax.net.ssl.keyStorePassword=password
  1. This property is 'true' by default, included here for completeness sake

Client side
javax.net.ssl.trustStore=truststore.jks
javax.net.ssl.trustStorePassword=password

4.2.3. Class loading

No dynamic class loading is performed.

4.2.4. Serialization whitelist

A serialization whitelist can be used by setting the following system property.

jminor.server.serializationFilterWhitelist=config/whitelist.txt

A whitelist can be created during a server dry-run by adding the following system property. The whitelist containing all classes deserialized during the run is written to disk on server shutdown.

jminor.server.serializationFilterDryRun=true
Example whitelist
[B
[C
[Ljava.lang.Object;
[Ljava.lang.String;
[Ljava.util.Map$Entry;
[Lnet.sf.jasperreports.engine.JRBand;
[Lnet.sf.jasperreports.engine.JRExpressionChunk;
[Lnet.sf.jasperreports.engine.JRField;
[Lnet.sf.jasperreports.engine.JRParameter;
[Lnet.sf.jasperreports.engine.JRQueryChunk;
[Lnet.sf.jasperreports.engine.JRVariable;
java.lang.Boolean
java.lang.Character
java.lang.Double
java.lang.Enum
java.lang.Float
java.lang.Integer
java.lang.Long
java.lang.Number
java.sql.Date
java.util.ArrayList
java.util.Arrays$ArrayList
java.util.Collections$SingletonList
java.util.Collections$SingletonMap
java.util.Date
java.util.HashMap
java.util.HashSet
java.util.LinkedHashMap
java.util.UUID
net.sf.jasperreports.engine.JRPropertiesMap
net.sf.jasperreports.engine.JasperReport
net.sf.jasperreports.engine.base.JRBaseBand
net.sf.jasperreports.engine.base.JRBaseBoxBottomPen
net.sf.jasperreports.engine.base.JRBaseBoxLeftPen
net.sf.jasperreports.engine.base.JRBaseBoxPen
net.sf.jasperreports.engine.base.JRBaseBoxRightPen
net.sf.jasperreports.engine.base.JRBaseBoxTopPen
net.sf.jasperreports.engine.base.JRBaseDataset
net.sf.jasperreports.engine.base.JRBaseElement
net.sf.jasperreports.engine.base.JRBaseElementGroup
net.sf.jasperreports.engine.base.JRBaseExpression
net.sf.jasperreports.engine.base.JRBaseExpressionChunk
net.sf.jasperreports.engine.base.JRBaseField
net.sf.jasperreports.engine.base.JRBaseLineBox
net.sf.jasperreports.engine.base.JRBaseParagraph
net.sf.jasperreports.engine.base.JRBaseParameter
net.sf.jasperreports.engine.base.JRBasePen
net.sf.jasperreports.engine.base.JRBaseQuery
net.sf.jasperreports.engine.base.JRBaseQueryChunk
net.sf.jasperreports.engine.base.JRBaseReport
net.sf.jasperreports.engine.base.JRBaseSection
net.sf.jasperreports.engine.base.JRBaseStaticText
net.sf.jasperreports.engine.base.JRBaseTextElement
net.sf.jasperreports.engine.base.JRBaseTextField
net.sf.jasperreports.engine.base.JRBaseVariable
net.sf.jasperreports.engine.design.JRReportCompileData
net.sf.jasperreports.engine.type.CalculationEnum
net.sf.jasperreports.engine.type.EvaluationTimeEnum
net.sf.jasperreports.engine.type.HorizontalTextAlignEnum
net.sf.jasperreports.engine.type.IncrementTypeEnum
net.sf.jasperreports.engine.type.OrientationEnum
net.sf.jasperreports.engine.type.PositionTypeEnum
net.sf.jasperreports.engine.type.PrintOrderEnum
net.sf.jasperreports.engine.type.ResetTypeEnum
net.sf.jasperreports.engine.type.RunDirectionEnum
net.sf.jasperreports.engine.type.SectionTypeEnum
net.sf.jasperreports.engine.type.SplitTypeEnum
net.sf.jasperreports.engine.type.StretchTypeEnum
net.sf.jasperreports.engine.type.WhenResourceMissingTypeEnum
org.jminor.common.Conjunction
org.jminor.common.db.condition.ConditionType
org.jminor.common.remote.Clients$DefaultConnectionRequest
org.jminor.common.user.User
org.jminor.common.version.Version
org.jminor.framework.db.condition.DefaultConditionSet
org.jminor.framework.db.condition.DefaultCustomCondition
org.jminor.framework.db.condition.DefaultEntityCondition
org.jminor.framework.db.condition.DefaultEntitySelectCondition
org.jminor.framework.db.condition.DefaultPropertyCondition
org.jminor.framework.domain.entity.DefaultEntity
org.jminor.framework.domain.entity.DefaultEntity$DefaultKey
org.jminor.framework.domain.entity.DefaultOrderBy
org.jminor.framework.domain.entity.DefaultOrderBy$DefaultOrderByProperty
org.jminor.plugin.jasperreports.model.JasperReportsWrapper

4.2.5. Security Policy

Security policy generated with ProGrade.

Requires this fix: Issue#29

Demo security policy
grant {
    permission java.io.FilePermission "../config/-", "read";
    permission java.io.FilePermission "../lib/-", "read";
    permission java.io.FilePermission "../logs/-", "read,write,delete";
    permission java.io.FilePermission "../reports/-", "read";
    permission java.io.FilePermission "../web", "read";
    permission java.io.FilePermission "jasperreports.properties", "read";
    permission java.io.FilePermission "mem:h2db.h2.db", "read";
    permission java.io.FilePermission "mem:h2db.mv.db", "read";
    permission java.io.FilePermission "net/sf/jasperreports/fonts/icons/icons.ttf", "read";
    permission java.io.FilePermission "net/sf/jasperreports/fonts/jasperreports-fonts.xml", "read";
    permission java.io.FilePermission "/tmp/*", "write,delete";
    permission java.lang.management.ManagementPermission "monitor";
    permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
    permission java.lang.RuntimePermission "accessClassInPackage.sun.misc";
    permission java.lang.RuntimePermission "accessDeclaredMembers";
    permission java.lang.RuntimePermission "createClassLoader";
    permission java.lang.RuntimePermission "getClassLoader";
    permission java.lang.RuntimePermission "getenv.JETTY_AVAILABLE_PROCESSORS";
    permission java.lang.RuntimePermission "getenv.JETTY_WORKER_INSTANCE";
    permission java.lang.RuntimePermission "getProtectionDomain";
    permission java.lang.RuntimePermission "setContextClassLoader";
    permission java.lang.RuntimePermission "shutdownHooks";
    permission java.net.SocketPermission "127.0.0.1:*", "accept,resolve";
    permission java.net.SocketPermission "localhost:2222", "listen,resolve";
    permission java.net.SocketPermission "localhost:4444", "listen,resolve";
    permission java.net.SocketPermission "localhost:8080", "listen,resolve";
    permission java.util.PropertyPermission "java.rmi.server.hostname", "write";
    permission java.util.PropertyPermission "java.rmi.server.randomIDs", "write";
    permission java.util.PropertyPermission "java.rmi.server.useCodebaseOnly", "write";
    permission java.util.PropertyPermission "javax.net.ssl.keyStorePassword", "write";
    permission java.util.PropertyPermission "javax.net.ssl.keyStore", "write";
    permission java.util.PropertyPermission "javax.net.ssl.trustStorePassword", "write";
    permission java.util.PropertyPermission "javax.net.ssl.trustStore", "write";
    permission java.util.PropertyPermission "jetty.git.hash", "write";
    permission java.util.PropertyPermission "jminor.*", "write";
    permission java.util.PropertyPermission "logback.configurationFile", "write";
    permission java.util.PropertyPermission "*", "read,write";
};

4.3. Configuration

4.3.1. Example configuration file

# Database configuration
jminor.db.type=h2
jminor.db.host=h2db
jminor.db.embeddedInMemory=true
jminor.db.useOptimisticLocking=true
jminor.db.initScript=\
    ../config/empdept/create_schema.sql,\
    ../config/chinook/create_schema.sql,\
    ../config/petstore/create_schema.sql,\
    ../config/world/create_schema.sql

# The admin user credentials, used by the server monitor application
jminor.server.admin.user=scott:tiger

# Client logging enabled by default
jminor.server.clientLoggingEnabled=true

# A domain model class for each application
jminor.server.domain.classes=\
    org.jminor.framework.demos.empdept.domain.EmpDept,\
    org.jminor.framework.demos.petstore.domain.Petstore,\
    org.jminor.framework.demos.chinook.domain.impl.ChinookImpl,\
    org.jminor.framework.demos.world.domain.World

# Any LoginProxy implementations
jminor.server.loginProxyClasses=\
    org.jminor.framework.demos.empdept.server.EmpDeptLoginProxy,\
    org.jminor.framework.demos.chinook.server.ChinookLoginProxy

# A connection pool based on this user is created on startup
jminor.server.pooling.startupPoolUsers=scott:tiger

# The port used by clients
jminor.server.port=2222

# The port for the admin interface, used by the server monitor
jminor.server.admin.port=4444

# RMI Registry port
jminor.server.registryPort=1099

# Any auxiliary servers to run along this server
jminor.server.auxiliaryServerClassNames=\
    org.jminor.framework.servlet.EntityServletServer

# A directory from which to serve files
jminor.server.http.documentRoot=../web

# The http port
jminor.server.http.port=8080

# Specifies whether or not to use https
jminor.server.http.secure=false

# The serialization whitelist to use for RMI deserialization
jminor.server.serializationFilterWhitelist=\
    ../config/serialization-whitelist.txt

# RMI configuration
java.rmi.server.hostname=localhost
java.rmi.server.randomIDs=true
java.rmi.server.useCodebaseOnly=true

# SSL configuration
javax.net.ssl.keyStore=../config/jminor_keystore.jks
javax.net.ssl.keyStorePassword=crappypass

# Used to connect to the server to shut it down
javax.net.ssl.trustStore=../config/jminor_truststore.jks
javax.net.ssl.trustStorePassword=crappypass

5. Server Monitor

5.1. Monitoring

The JMinor Server Monitor provides a way to introspect the server.

6. Modules

6.1. Common

Common classes used throughout the framework.

jminor-common-core

Dependency graph
dependency graph

jminor-common-db

Dependency graph
dependency graph

jminor-common-model

Dependency graph
dependency graph

jminor-common-remote

Dependency graph
dependency graph

jminor-common-remote-http

Dependency graph
dependency graph

6.2. DBMS

Database specific implementation classes.

jminor-dbms-derby

Dependency graph
dependency graph

jminor-dbms-h2database

Dependency graph
dependency graph

jminor-dbms-hsqldb

Dependency graph
dependency graph

jminor-dbms-mariadb

Dependency graph
dependency graph

jminor-dbms-mysql

Dependency graph
dependency graph

jminor-dbms-oracle

Dependency graph
dependency graph

jminor-dbms-postgresql

Dependency graph
dependency graph

jminor-dbms-sqlite

Dependency graph
dependency graph

jminor-dbms-sqlserver

Dependency graph
dependency graph

6.3. Framework

The framework itself.

jminor-framework-domain

Dependency graph
dependency graph

jminor-framework-domain-test

Dependency graph
dependency graph

jminor-framework-db-core

Dependency graph
dependency graph

jminor-framework-db-local

Dependency graph
dependency graph

jminor-framework-db-remote

Dependency graph
dependency graph

jminor-framework-db-http

Dependency graph
dependency graph

jminor-framework-model

Dependency graph
dependency graph

jminor-framework-server

Dependency graph
dependency graph

jminor-framework-servlet

Dependency graph
dependency graph

6.4. Swing

Swing client implementation.

jminor-swing-common-model

Dependency graph
dependency graph

jminor-swing-common-ui

Dependency graph
dependency graph

jminor-swing-common-tools

Dependency graph
dependency graph

jminor-swing-common-tools-ui

Dependency graph
dependency graph

jminor-swing-framework-model

Dependency graph
dependency graph

jminor-swing-framework-ui

Dependency graph
dependency graph

jminor-swing-framework-ui-test

Dependency graph
dependency graph

jminor-swing-framework-tools

Dependency graph
dependency graph

jminor-swing-framework-tools-ui

Dependency graph
dependency graph

jminor-swing-framework-server-monitor

Dependency graph
dependency graph

6.5. JavaFX

JavaFX client implementation (still quite experimental).

jminor-javafx-framework

Dependency graph
dependency graph

6.6. Plugins

6.6.1. Logging

jminor-plugin-jul-proxy
Dependency graph
dependency graph
jminor-plugin-log4j-proxy
Dependency graph
dependency graph
jminor-plugin-logback-proxy
Dependency graph
dependency graph

6.6.2. Connection pools

jminor-plugin-hikari-pool
Dependency graph
dependency graph
jminor-plugin-tomcat-pool
Dependency graph
dependency graph

6.6.3. Reporting

jminor-plugin-jasperreports
Dependency graph
dependency graph
jminor-plugin-nextreports
Dependency graph
dependency graph

6.6.4. JSON

Dependency graph
dependency graph
jminor-plugin-json
Dependency graph
dependency graph

6.6.5. Other

jminor-plugin-credentials-server
Dependency graph
dependency graph
jminor-plugin-imagepanel
Dependency graph
dependency graph