JMinor Application Framework

As simple as possible but not simpler

User Tools

Site Tools


documentation:manual

JMinor Manual

The basics

Common classes

Two common classes used throughout the framework are the Event and State classes and their respective observers EventObserver and StateObserver and listeners EventListener and EventInfoListener.

Event

The Event class is a simple event implementation used throughout the framework to signal object state changes or to indicate that an action is about to start or has ended. Classes typically publish their events via public addListener methods. Events are triggered by calling the fire() method, with or without an eventInfo parameter.

Events are instantiated via the Events factory class.

To listen to Events you use the EventListener or EventInfoListener interfaces.

Event event = Events.event();
 
event.addListener(new EventAdapter<String>() {
  public void eventOccurred(final String eventInfo) {
    System.out.println("Event happened: " + eventInfo);
  }
});
 
event.fire();//writes out "Event happened: null";
event.fire("info");//writes out "Event happened: info";

State

The State class encapsulates a boolean state (active/inactive) and provides a StateObserver:

States are instantiated via the States factory class.

State state = States.state();
 
state.addListener(new EventAdapter() {
  public void eventOccurred() {
    System.out.println("State changed");
  }
});
 
state.setActive(true);//writes out "State changed";

Any Action object can be linked to a State object via the UiUtil.linkToEnabledState method, whereas the action's enabled status is synchronized with the state.

State state = States.state();
 
Action action = new AbstractAction("action") {
  public void actionPerformed(final ActionEvent e) {}
};
 
UiUtil.linkToEnabledState(state, action);
 
//action.isEnabled() now returns false, since State objects are by default inactive.
 
state.setActive(true);
 
//action.isEnabled() now returns true.

Domain model

Introduction

In JMinor you can define the domain model in a single Java class with no XML involved, which helps during debugging and makes the domain model readable.

The domain model is based around two interfaces, org.jminor.framework.domain.Entity, which represents a row in a table, and org.jminor.framework.domain.Property, which represents columns. The org.jminor.framework.domain.Entities class serves both as a factory for Entity objects as well as a central repository for storing the domain model definition.

Entity

An Entity object represents a row in a table and maps each column value to its respective property. Each Entity object is uniquely identified by its entityID, which is a string identifying the table on which the Entity is based, usually the table name, and its primary key.

Entity ID constants

For each table you define a string constant containing the full table name, i.e. 'schema.table'. This constant is used when defining the entity based on that table and serves as the entity identifier. This entityID is used as table name when constructing SQL queries.

/**
 * The Store class defines the domain model for the Store application
 */
public class Store {
  //the full name of the table, will serve as the entityID for the Entity based on this table
  public static final String T_ADDRESS = "store.address";
 
  ...
 
  //the full name of the table, will serve as the entityID for the Entity based on this table
  public static final String T_CUSTOMER = "store.customer";
 
  ...

Property

Each column in a table is defined by the Property class or one of its subclasses. The Properties class provides factory methods for constructing Property objects, taking as parameters the property Id, the caption and data type. The following properties can be set by using chained method calls: hidden, updatable, readOnly, nullable, defaultValue, maxLength, preferredWidth, description, mnemonic, columnHasDefaultValue, format

Properties.columnProperty(PROPERTY_ID, Types.INTEGER, "Caption").setNullable(false).setDescription("This is a non-nullable property")

Supported data types

JMinor supports the following column data types:

  • Integer (java.sql.Types.INTEGER)
  • Double (java.sql.Types.DOUBLE)
  • Long (java.sql.Types.BIGINT)
  • Timestamp (java.sql.Types.TIMESTAMP)
  • Date (java.sql.Types.DATE)
  • Time (java.sql.Types.TIME)
  • String (java.sql.Types.VARCHAR)
  • Boolean (java.sql.Types.BOOLEAN)
  • Character (java.sql.Types.CHAR)
  • Blob (java.sql.Types.BLOB)
  • Entity (java.sql.Types.REF)

Property ID constants

For each column you define a string constant containing the column name, i.e. 'first_name'. This constant is used when defining the property based on that column and serves as the property identifier. This propertyID is used as column name when constructing SQL queries unless the column name is set via setColumnName().

/**
 * The Store class defines the domain model for the Store application
 */
public class Store {
  //the full name of the table, will serve as the entityID for the Entity based on this table
  public static final String T_ADDRESS = "store.address";
 
  //Property constant identifying the column ID in the ADDRESS table
  public static final String ADDRESS_ID = "id";
  //Property constant identifying the column STREET in the ADDRESS table
  public static final String ADDRESS_STREET = "street";
  //Property constant identifying the column CITY in the ADDRESS table
  public static final String ADDRESS_CITY = "city";
 
  //the full name of the table, will serve as the entityID for the Entity based on this table
  public static final String T_CUSTOMER = "store.customer";
 
  //Property constant identifying the column ID in the CUSTOMER table
  public static final String CUSTOMER_ID = "id";
  //Property constant identifying the column FIRST_NAME in the CUSTOMER table
  public static final String CUSTOMER_FIRST_NAME = "first_name";
  //Property constant identifying the column LAST_NAME in the CUSTOMER table
  public static final String CUSTOMER_LAST_NAME = "last_name";
  //Property constant identifying the foreign key referencing the ADDRESS entity,
  //the value is somewhat arbitrary since it does not map to a column
  public static final String CUSTOMER_ADDRESS_FK = "address_fk";
  //Property constant identifying the column referencing the STORE.ADDRESS table
  public static final String CUSTOMER_ADDRESS_ID = "address_id";
  //Property constant identifying the column IS_ACTIVE in the CUSTOMER table
  public static final String CUSTOMER_IS_ACTIVE = "is_active";
  //Property constant identifying the denormalized column CITY in the CUSTOMER table
  public static final String CUSTOMER_CITY = "city";
  //Property constant identifying a derived property CUSTOMER entity
  public static final String CUSTOMER_DERIVED = "derived";
 
  ...

Property

Property and its subclasses is used to represent entity properties, these can be transient or based on table columns.

Properties.transientProperty(CUSTOMER_TOKEN, Types.VARCHAR, "Token")

ColumnProperty

ColumnProperty is used to represent properties that are based on table columns.

Properties.columnProperty(CUSTOMER_LAST_NAME, Types.VARCHAR, "Last name")
Primary key

Entities must have at least one primary key column property.

The only requirement is that the primary key properties represent a unique column combination for the underlying table, it does not have to correspond to an actual table primary key, although that is of course preferable. JMinor does not enforce uniqueness for these properties, so a unique or primary key on the corresponding table columns is strongly recommended.

Properties.primaryKeyProperty(CUSTOMER_ID)//by default Types.INTEGER and primaryKeyIndex 0

If the primary key is comprised of more than one column you must set the primary key index.

Properties.columnProperty(ID_1, Types.INTEGER).setPrimaryKeyIndex(0),
Properties.columnProperty(ID_2, Types.INTEGER).setPrimaryKeyIndex(1),

ForeignKeyProperty

ForeignKeyProperty is a wrapper property used to indicate a foreign key relation. These foreign keys refer to the primary key of the referenced entity and must be constructed accordingly in case of non-trivial primary keys.

//referring to an entity with a single column primary key
Properties.foreignKeyProperty(CUSTOMER_ADDRESS_FK, "Address", T_ADDRESS,
        Properties.columnProperty(CUSTOMER_ADDRESS_ID))
 
//referring to an entity with a dual column primary key, one more parameter is
//required, an array containing the ID's of the properties being referenced
//in the master entity
Properties.foreignKeyProperty(MASTER_FK, "Master", T_MASTER,
        new Property.ColumnProperty[] {
                Properties.columnProperty(MASTER_ID_1),
                Properties.columnProperty(MASTER_ID_2)
        }, new String[] {
                MASTER_PK_PROP_1,
                MASTER_PK_PROP_2
        });

In this example CUSTOMER_ADDRESS_FK is the ID of the foreign key property and can be used to retrieve the actual entity being referred to.

Entity address = customer.getForeignKey(CUSTOMER_ADDRESS_FK);

CUSTOMER_ADDRESS_ID is the actual column used as foreign key and retrieving that will simply return the reference id value.

Integer addressId = customer.getInteger(CUSTOMER_ADDRESS_ID);

By default one level of foreign key values is eagerly fetched during selects, this can be overridden via setFetchDepth(). Note that the example below does not really make sense since the ADDRESS entity doesn't have any foreign keys, but if id did the entities referred to via these keys would be eagerly loaded.

//referring to an entity with a single column primary key
Properties.foreignKeyProperty(CUSTOMER_ADDRESS_FK, "Address", T_ADDRESS,
        Properties.columnProperty(CUSTOMER_ADDRESS_ID)).setFetchDepth(2)

Boolean Properties

ColumnProperty is used to represent boolean columns, which can be based on many different underlying data types, the most common being an integer. If the underlying DBMS supports boolean columns natively you can simply use the ColumnProperty base class with Types.BOOLEAN as type. When creating a boolean property based on a non-boolean data type, a character or integer for example, you must specify the values translating to true and false.

Properties.booleanProperty(CUSTOMER_IS_ACTIVE, Types.INTEGER, "Is active", 1, 0)
Properties.booleanProperty(CUSTOMER_IS_ACTIVE, Types.VARCHAR, "Is active", "true", "false")
Properties.booleanProperty(CUSTOMER_IS_ACTIVE, Types.CHAR, "Is active", 'T', 'F')

DenormalizedProperty

DenormalizedProperty is used for columns that should automatically get their value from a column in a referenced table. This property automatically gets the value from the column in the referenced table when the corresponding reference property value is set.

Properties.denormalizedProperty(CUSTOMER_CITY, CUSTOMER_ADDRESS_FK,
        Entities.getProperty(T_ADDRESS, ADDRESS_CITY), "City")

The property is not kept in sync if the denormalized property is updated in the referenced entity.

Entity address = Entities.entityInstance(T_ADDRESS);
address.put(ADDRESS_CITY, "Syracuse");
 
Entity customer = Entities.entityInstance(T_CUSTOMER);
customer.put(CUSTOMER_ADDRESS_FK, address);
 
customer.get(CUSTOMER_CITY);//returns "Syracuse"
 
//NB
address.put(ADDRESS_CITY, "Canastota");
customer.get(CUSTOMER_CITY, still returns "Syracuse"
 
customer.put(CUSTOMER_ADDRESS_FK, address);//set the referenced value again
customer.get(CUSTOMER_CITY);//now this returns "Canastota"

DenormalizedViewProperty

DenormalizedViewProperty is used to show property values of referenced entities, these do not correspond to an underlying column and are as such read only.

Properties.denormalizedViewProperty(DEPARTMENT_LOCATION, EMPLOYEE_DEPARTMENT_FK,
        Entities.getProperty(T_DEPARTMENT, DEPARTMENT_LOCATION), "Location")

SubqueryProperty

SubqueryProperty is used to represent properties which get their value from a subquery returning a single value.

Properties.subqueryProperty(SUBQUERY_PROPERTY_ID, Types.VARCHAR, "Caption",
        "select field from schema.table where id = reference_id"))

TransientProperty

TransientProperty is used to represent a property which has no underlying column, these properties all have a default value of null and can set and retrieved just like normal properties.

DerivedProperty

DerivedProperty is used to represent a transient property which value is derived from one or more linked property values. The value of a derived property is provided via a DerivedProperty.Provider implementation as shown below..

Properties.derivedProperty(PROPERTY_ID, Types.INTEGER, "Derived value",
         new Property.DerivedProperty.Provider() {
           public Object getValue(final Map<String, Object> linkedValues) {
              final Integer linkedOne = (Integer) linkedValues.get(SOURCE_PROPERTY_ID_1);
              final Integer linkedTwo = (Integer) linkedValues.get(SOURCE_PROPERTY_ID_2);
 
              return linkedOne + linkedTwo;
            }
         }, SOURCE_PROPERTY_ID_1, SOURCE_PROPERTY_ID_2),

Example: Chinook domain

Entities

The Entities class serves as a factory class for Entity objects as well as a central repository of entity meta-information, each entity type must be statically initialized, by calling Entities.define for each entity.

//public class Store continued
 
  static {
    //Defining the entity that represents the table STORE.ADDRESS,
    //with a property for each column in the table, identified by their respective constants
    Entities.define(T_ADDRESS,
        Properties.primaryKeyProperty(ADDRESS_ID),
        Properties.columnProperty(ADDRESS_STREET, Types.VARCHAR, "Street"),
        Properties.columnProperty(ADDRESS_CITY, Types.VARCHAR, "City"))
        .setStringProvider(new StringProvider<String>(ADDRESS_STREET).addText(" - ").addValue(ADDRESS_CITY)
    );
 
    //Defining the entity that represents the table STORE.CUSTOMER,
    //with a property for each column in the table, identified by their respective constants
    Entities.define(T_CUSTOMER,
        Properties.primaryKeyProperty(CUSTOMER_ID),
        Properties.columnProperty(CUSTOMER_FIRST_NAME, Types.VARCHAR, "First name").setDescription("The first name of the customer"),
        Properties.columnProperty(CUSTOMER_LAST_NAME, Types.VARCHAR, "Last name").setDescription("The last name of the customer"),
        Properties.foreignKeyProperty(CUSTOMER_ADDRESS_FK, "Address", T_ADDRESS,
            Properties.columnProperty(CUSTOMER_ADDRESS_ID)).setNullable(false),
        Properties.booleanProperty(CUSTOMER_IS_ACTIVE, Types.VARCHAR, "Is active", "true", "false").setDefaultValue(true),
        Properties.denormalizedProperty(CUSTOMER_CITY, T_ADDRESS,
            Entities.getProperty(T_ADDRESS, ADDRESS_CITY), "City"),
        Properties.transientProperty(CUSTOMER_TRANSIENT, Types.VARCHAR, "Transient"))
        .setStringProvider(new StringProvider<String>(CUSTOMER_LAST_NAME).addText(", ").addValue(CUSTOMER_FIRST_NAME)
    );
  }
}

Entities.StringProvider

The Entities.StringProvider class is for providing toString() implementations for each Entity type. This value is f. ex. used when the entity instance is shown in a ComboBox or as a reference property value in table views.

See the Entities.StringProvider API doc for further information.

Helper classes

The following classes can come in handy when working with entities.

Entities.Validator

A default Entity.Validator implementation which provides basic range and null validation, can be overridden to provide further validations.

Entities.define("entityID", [...]).setValidator(new Entities.Validator() {
      @Override
      public void validate(Entity entity, String propertyID, final int action) throws ValidationException {
        super.validate(entity, propertyID, action);
 
        Object value = entity.get(propertyID);
 
        if (!isValid(value)) {
          throw new ValidationException("Value '" + value + "' is invalid for property: " + propertyID);
        }
      }
    });

Entity.BackgroundColorProvider

Provides the background color for entity property cells when displayed in a table.

Entities.define("entityID", [...]).setBackgroundColorProvider(new Entity.BackgroundColorProvider() {
      public Object getBackgroundColor(final Entity entity, final Property property) {
        if (property.is("colorPropertyID") && entity.getString("colorPropertyID").equals("CYAN")) {
          return Color.CYAN;
        }
        return null;
      }
    });

Example: EmpDept domain

Entities in action

Using the Entity class is rather straight forward.

//initialize the domain model by instantiating the domain model class,
//loading it by name would also suffice
new Store();
 
//Initialize a database object using the credentials scott/tiger and application identifier TestApp
EntityConnectionProvider connectionProvider = EntityConnectionProviders.createConnectionProvider(
             new User("scott", "tiger"), "TestApp");
 
EntityConnection connection = connectionProvider.getConnection();
 
//Initialize a new entity representing the table STORE.ADDRESS
Entity address = Entities.entity(Store.T_ADDRESS);
 
//Set the value of the column ID to 42
address.put(Store.ADDRESS_ID, 42);
//Set the value of the column STREET to "Elm Street"
address.put(Store.ADDRESS_STREET, "Elm Street");
//Set the value of the column CITY to "Seattle"
address.put(Store.ADDRESS_CITY, "Seattle");
 
//Insert the address entity
connection.insert(Arrays.asList(address));
 
//Initialize a new entity representing the table STORE.CUSTOMER
Entity customer = Entities.entity(Store.T_CUSTOMER);
 
//Set the value of the column ID to 42
customer.put(Store.CUSTOMER_ID, 42);
//Set the value of the column FIRST_NAME to John
customer.put(Store.CUSTOMER_FIRST_NAME, "John");
//Set the value of the column LAST_NAME to Doe
customer.put(Store.CUSTOMER_LAST_NAME, "Doe");
//Set the reference value ADDRESS
customer.put(Store.CUSTOMER_ADDRESS_FK, address);
//Set the value of the column IS_ACTIVE to true
customer.put(Store.CUSTOMER_IS_ACTIVE, true);
 
//Insert the entity representing the customer John Doe,
//recieving the primary key of the new record in a list as a return value
List<Entity.Key> key = connection.insert(Arrays.asList(customer));
 
//Retrieve the ID value
Integer id = customer.getInteger(Store.CUSTOMER_ID);
//Retrieve the first name value
String firstName = customer.getString(Store.CUSTOMER_FIRST_NAME);
 
//Select the entity from the database by primary key
Entity customerByKey = connection.selectSingle(key.get(0));
 
//Select entities representing the table STORE.CUSTOMER by first name
List<Entity> entitiesByFirstName = connection.selectMany(Store.T_CUSTOMER, Store.CUSTOMER_FIRST_NAME, "Björn");

EntityGenerator

Using the EntityGenerator tool you can quickly generate a domain class containing the property ID constants for a given database schema as well as basic entity definitions.

The required command line arguments are: schema_name domain_class_package_name username password Additionally you can add a comma seperated list of tables to include.

|Example
java -Djminor.db.type=h2 -Djminor.db.embedded=true -Djminor.db.host=h2db/h2 -cp dist/jminor.jar:lib/h2-1.1.114.jar:lib/log4j-1.2.15.jar:lib/jcalendar-1.3.2.jar org.jminor.swing.framework.ui.EntityGenerator PETSTORE org.petstore.domain.Petstore scott tiger

Examples

Domain model test

Introduction

To unit test the CRUD operations on the domain model extend EntityTestUnit.

The unit tests are run within a single transaction which is rolled back after the test finishes, so these tests are pretty much guaranteed to leave no junk data behind.

EntityTestUnit

When you extend the EntityTestUnit class you must implement the loadDomainModel() method which should simply initialize the domain model: loadDomainModel.

The following methods all have default implementations which are based on randomly created property values, based on the constraints set in the domain model, override if the default ones are not working.

  • initializeReferenceEntity should initialize return an instance of the given entity type to use for a foreign key reference required for inserting the entity being tested.
  • initializeTestEntity should return a entity to use as basis for the unit test, that is, the entity that should be inserted, selected, updated and finally deleted.
  • modifyEntity should simply leave the entity in a modified state so that it can be used for update test, since the db layer throws an exception if an unmodified entity is updated. If modifyEntity returns an unmodified entity, the update test is skipped.

To run the full CRUD test for a domain entity you need to call the testEntity(String entityID) method with the ID of the given entity as parameter. You can either create a single testDomain() method and call the testEntity method in turn for each entityID or create a single testEntityName for each domain entity, as we do in the example below.

public class TestStore extends EntityTestUnit {
 
  @Test //a unit test method for the address entity
  public void address() throws Exception {
    //The testEntity method performs basic insert/select/update/delete tests for the given entity
    testEntity(Store.T_ADDRESS);
  }
 
  @Test //a unit test method for the customer entity
  public void customer() throws Exception {
    testEntity(Store.T_CUSTOMER);
  }
 
  @Override
  protected void loadDomainModel() {
    //Load the domain model by instantiating the Store class,
    //which contains the entity definitions. Loading the class by name
    //would suffice since the initialization is performed within a static
    //initialization block.
    new Store();
  }
 
  @Override
  protected void initializeReferenceEntity(String entityID) throws Exception {
    //see if the currently running test requires an ADDRESS entity
    if (entityIDs.contains(Store.T_ADDRESS)) {
      Entity address = new Entity(Store.ADDRESS);
      address.put(Store.ADDRESS_ID, 21);
      address.put(Store.ADDRESS_STREET, "One Way");
      address.put(Store.ADDRESS_CITY, "Sin City");
 
      return address;
    }
 
    return super.initializeReferenceEntity(entityID);
  }
 
  @Override
  protected Entity initializeTestEntity(String entityID) {
    if (entityID.equals(Store.T_ADDRESS)) {
      //Initialize a entity representing the table STORE.ADDRESS,
      //which can be used for the testing
      Entity address = new Entity(Store.T_ADDRESS);
      address.put(Store.ADDRESS_ID, 42);
      address.put(Store.ADDRESS_STREET, "Street");
      address.put(Store.ADDRESS_CITY, "City");
 
      return address;
    }
    else if (entityID.equals(Store.T_CUSTOMER)) {
      //Initialize a entity representing the table STORE.CUSTOMER,
      //which can be used for the testing
      Entity customer = new Entity(Store.T_CUSTOMER);
      customer.put(Store.CUSTOMER_ID, 42);
      customer.put(Store.CUSTOMER_FIRST_NAME, "Robert");
      customer.put(Store.CUSTOMER_LAST_NAME, "Ford");
      //the getReferenceEntity() method returns the entity initialized in initializeReferenceEntities()
      customer.put(Store.CUSTOMER_ADDRESS_FK, getReferenceEntity(Store.T_ADDRESS));
      customer.put(Store.CUSTOMER_IS_ACTIVE, true);
 
      return customer;
    }
 
    return null;
  }
 
  @Override
  protected void modifyEntity(Entity testEntity) {
    if (testEntity.is(Store.T_ADDRESS)) {
      testEntity.put(Store.ADDRESS_STREET, "New Street");
      testEntity.put(Store.ADDRESS_CITY, "New City");
    }
    else if (testEntity.is(Store.T_CUSTOMER)) {
      //It is sufficient to change the value of a single property, but the more the merrier
      testEntity.put(Store.CUSTOMER_FIRST_NAME, "Jesse");
      testEntity.put(Store.CUSTOMER_LAST_NAME, "James");
      testEntity.put(Store.CUSTOMER_IS_ACTIVE, false);
    }
  }
}

Examples

EntityModel

Introduction

The EntityEditModel interface defines the CRUD business logic used by the EntityEditPanel class when entities are being edited, and must be defined for each entity requiring a CRUD user interface. The EntityEditModel works with a single entity instance, called the active entity, which can be set via the setEntity(Entity entity) method and retrieved via getEntityCopy(). The EntityEditModel interface exposes a number of methods for manipulating as well as querying the property values of the active entity, via the ValueMapEditModel interface which it extends

public class CustomerEditModel extends SwingEntityEditModel {
  public CustomerEditModel(EntityConnectionProvider connectionProvider) {
    super(Store.T_CUSTOMER, connectionProvider);
  }
}
public class AddressEditModel extends SwingEntityEditModel {
  public AddressModel(EntityConnectionProvider connectionProvider) {
    super(Store.T_ADDRESS, connectionProvider);
    addDetailModel(new CustomerModel(connectionProvider));
  }
}
//Initialize a database provider object using the credentials scott/tiger and application identifier TestApp
EntityConnectionProvider connectionProvider =
       EntityConnectionProviders.createConnectionProvider(new User("scott", "tiger"), "TestApp");
 
CustomerEditModel customerModel = new CustomerEditModel(connectionProvider);
 
customerModel.put(Store.CUSTOMER_ID, 42);
customerModel.put(Store.CUSTOMER_FIRST_NAME, "Björn");
customerModel.put(Store.CUSTOMER_LAST_NAME, "Sigurðsson");
customerModel.put(Store.CUSTOMER_IS_ACTIVE, true);
 
//inserts the active entity
List<Entity.Key> primaryKeys = customerModel.insert();
 
//select the customer we just inserted
Entity customer = connectionProvider.getConnection().selectSingle(primaryKeys.get(0));
 
//set the customer as the entity to edit in the edit model
customerModel.setEntity(customer);
 
//modify some property values
customerModel.put(Store.CUSTOMER_FIRST_NAME, "John");
customerModel.put(Store.CUSTOMER_LAST_NAME, "Doe");
 
//updates the active entity
customerModel.update();
 
//deletes the active entity
customerModel.delete();

Detail models

Directly adding a detail models is a trivial matter, the framework handles everything as long as the master/detail relationship is defined in the domain model.

addDetailModel(new CustomerModel(connectionProvider);

Table model

Each EntityModel can contain a single EntityTableModel instance. This table model can be created automatically by the EntityModel or supplied via a constructor argument in case of a specialized implementation.

static class TestTableModel extends SwingEntityTableModel {
  public TestTableModel(EntityConnectionProvider connectionProvider) {
    super("entityID", connectionProvider);
  }
}
 
static class TestModel extends SwingEntityModel {
  public TestModel(EntityConnectionProvider connectionProvider) {
    super(new TestTableModel(connectionProvider));
  }
}

Edit model

Each EntityModel contains a single EntityEditModel instance. This edit model can be created automatically by the EntityModel or supplied via a constructor argument in case of a specialized implementation.

static class TestEditModel extends SwingEntityEditModel {
  public TestEditModel(EntityConnectionProvider connectionProvider) {
    super("entityID", connectionProvider);
  }
 
  @Override
  public void validate(Entity entity, String propertyID, int action) throws ValidationException {
    ...
  }
}
 
static class TestModel extends SwingEntityModel {
  public TestModel(EntityConnectionProvider connectionProvider) {
    super(new TestEditModel(connectionProvider));
  }
}

Input validation

To validate input you must override the validate() method in the EntityEditModel, this method should throw a ValidationException in case the given value is invalid. The default validate() implementation performs null value validation based on the result of isNullable() in the underlying property.

@Override
public void validate(final Entity entity, final Property property, final int action) throws ValidationException {
  if (property.is(Store.CUSTOMER_FIRST_NAME)) {
    final String firstName = (String) entity.get(property);
    if (firstName != null && (firstName.length() < 1 || firstName.length() > 100))
      throw new ValidationException(property, value, "First name length must be between 1 and 100");
  }
  super.validate(entity, property, value);
}

Event binding

The EntityModel, EntityEditModel and EntityTableModel classes expose a number of addListener methods.

The following example prints, to the standard output, all changes made to a given property as well as a message indicating that a refresh has started.

protected void bindEvents() {
  getTableModel().addRefreshStartedListener(new EventListener() {
    public void eventOccurred() {
      System.out.println("Refresh is about to start");
    }
  });
  getEditModel().addValueListener(EmpDept.EMPLOYEE_DEPARTMENT_FK, new ValueChangeListener() {
    protected void valueChanged(ValueChangeEvent event) {
      System.out.println("Property " + e.getKey() + " changed from " + e.getOldValue() + " to " + e.getNewValue());
    }
  });
}

Examples

EntityApplicationModel

Introduction

The EntityApplicationModel class serves as the base for the application. Its main purpose is to hold references to the root EntityModel instances used by the application.

When implementing this class you must provide a constructor taking a single EntityConnectionProvider instance as argument, as seen below. One method must be implemented, loadDomainModel(). This method should load the domain model, either by instantiating the domain model definition class or by loading it by name.

public class StoreApplicationModel extends SwingEntityApplicationModel {
 
  public StoreApplicationModel(final EntityConnectionProvider connectionProvider) {
    super(connectionProvider);
    addMainApplicationModel(new CustomerModel(connectionProvider));
  }
 
  @Override
  protected void loadDomainModel() {
    new Store();
  }
}

Application load testing

Introduction

The application load testing harness is used to see how your application, server and database handle multiple concurrent users. This is done by extending the abstract class org.jminor.framework.model.EntityLoadTestModel.

public class StoreLoadTest extends EntityLoadTestModel<StoreAppModel> {
 
  public StoreLoadTest() {
    super(new User("scott", "tiger"));
  }
 
  @Override
  protected void loadDomainModel() {
    new Store();
  }
 
  @Override
  protected void performWork(final StoreAppModel application) {
    try {
      final EntityModel customerModel = application.getMainApplicationModels().iterator().next();
      customerModel.getTableModel().clearSelection();
      customerModel.refresh();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
 
  @Override
  protected StoreAppModel initializeApplication() {
    return new StoreAppModel(new EntityConnectionRemoteProvider(getUser(), "scott@"+new Object(), getClass().getSimpleName()));
  }
 
  public static void main(String[] args) {
    new LoadTestPanel(new StoreLoadTest()).showFrame();
  }
}

Examples

EntityPanel

Introduction

The EntityPanel is the base UI class for working with entity instances. It usually consists of an EntityTablePanel, an EntityEditPanel, which contains the controls (text fields, combo boxes and such) for editing an entity instance and a set of detail panels representing the entities having a master/detail relationship with the underlying entity.

EntityEditPanel

When instantiating an EntityPanel you can supply an EntityEditPanel instance as a parameter. When implementing an EntityEditPanel you must implement the initializeUI() method, in which you should initialize the edit panel UI. The EntityEditPanel class exposes methods for creating input components and binding them with the underlying EntityEditModel instance.

public class CustomerEditPanel extends EntityEditPanel {
 
  public CustomerEditPanel(final EntityEditModel editModel) {
    super(editModel);
  }
 
  @Override
  protected void initializeUI() {
    //the firstName field should receive the focus whenever the panel is initialized
    setInitialFocusProperty(Store.CUSTOMER_FIRST_NAME);
 
    setLayout(new GridLayout(4,1));
    createTextField(Store.CUSTOMER_FIRST_NAME);
    createTextField(Store.CUSTOMER_LAST_NAME);
    createForeignKeyComboBox(Store.CUSTOMER_ADDRESS_FK);
    createCheckBox(Store.CUSTOMER_IS_ACTIVE, null, false);
 
    //the createControlPanel method creates a panel containing the
    //component associated with the property as well as a JLabel with the
    //property caption as defined in the domain model
    addPropertyPanel(Store.CUSTOMER_FIRST_NAME);
    addPropertyPanel(Store.CUSTOMER_LAST_NAME);
    addPropertyPanel(Store.CUSTOMER_ADDRESS_FK);
    addPropertyPanel(Store.CUSTOMER_IS_ACTIVE);
  }
}

Detail panels

Adding a detail panel is done with a single method call, but note that the underlying EntityModel must contain the correct detail model for the detail panel, in this case a CustomerModel instance, see detail models.

public TestPanel extends EntityPanel {
  public TestPanel(EntityModel model) {
    super("caption", model);
    addDetailPanel(new CustomerPanel(model.getDetailModel(CustomerModel.class)));
  }
}

Custom actions

The action mechanism used throughout the JMinor framework is based on the Control class and its subclasses and the ControlSet class which, as the name suggests, represents a set of controls. There are two static utility classes for creating and presenting controls, Controls and ControlProvider respectively.

Controls

Provides methods for initializing MethodControl objects as well as ToggleBeanValueLink objects.

public class MethodControlTest {
 
  public void printString() {
    System.out.println("printing a string");
  }
 
  public static void main(String[] args) {
    MethodControlTest testObj = new MethodControlTest();
    MethodControl control = Controls.methodControl(testObj, "printString", "Print string");
    //calls testObj.printString()
    control.actionPerformed(new ActionEvent(testObj, 0, "actionPerformed"));
  }
}

ControlProvider

The ControlProvider class provides static factory methods for creating UI components based on Control objects.

public class MethodControlTest {
 
  public void printString() {
    System.out.println("printing a string");
  }
 
  public static void main(String[] args) {
    MethodControlTest testObj = new MethodControlTest();
    MethodControl control = Controlsy.methodControl(testObj, "printString", "Print string");
    JButton printButton = ControlProvider.createButton(control);
 
    //calls testObj.printString()
    printButton.doClick();
  }
}

Adding a print action

The most common place to add a custom control is the table popup menu, f.ex. an action for printing reports. The table popup menu is based on the ControlSet returned by the getTablePopupControlSet() method in the EntityPanel class which in turn uses the ControlSet returned by the getPrintControls() method in the same class for constructing the print popup submenu. So, to add a custom print action you override the getPrintControls() method and return a ControlSet containing the action.

@Override
public ControlSet getPrintControls() {
  ControlSet printControls = new ControlSet("Print");
  //creates a MethodControl which calls the viewReport method in this class on activation
  printControls.add(Controls.methodControl(this, "viewReport", "Report"));
  //add the default print table control as well
  printControls.add(getControl(PRINT));
 
  return printControls;
}

EntityApplicationPanel

Examples

Reporting with JasperReports

Introduction

JMinor uses a plugin oriented approach to report viewing, and provides an implementation for JasperReports.

With the JMinor JasperReports plugin you can either design your report based on a SQL query in which case you use the JasperReportsWrapper class, which facilitates the report being filled using the active database connection or you can design your report around the JRDataSource implementation provided by the JasperReportsEntityDataSource class, which is constructed around an iterator.

The EntityPanel class provides straight forward methods for viewing reports using methods provided by the EntityModel class for filling them. Both these classes rely on static utility classes for doing the actual work so you are not bound by the EntityPanel and EntityModel classes for viewing reports, they simply provide the easiest way of doing so.

JDBC Reports

Using a report based on a SQL query, JasperReportsWrapper and JasperReportsUIWrapper is the simplest way of viewing a report using JMinor, just add a method similar to the one below to a EntityPanel subclass. You can then create an action calling that method and put it in for example the table popup menu as described in the adding a print action section.

public void viewCustomerReport() throws Exception {
  List<Entity> selectedCustomers = getModel().getTableModel().getSelectedItems();
  if (selectedCustomers.getSize() == 0)
    return;
 
  String reportPath = System.getProperty(Configuration.REPORT_PATH)
          + "/customer_report.jasper";
  Collection<Object> customerIds =
          EntityUtil.getValues(selectedCustomers, Store.CUSTOMER_ID);
  HashMap<String, Object> reportParameters = new HashMap<String, Object>();
  reportParameters.put("CUSTOMER_IDS", customerIds);
 
  viewJdbcReport(new JasperReportsWrapper(reportPath, reportParameters), new JasperReportsUIWrapper(),  "Customer Report");
}

JRDataSource Reports

The JRDataSource implementation provided by the JasperReportsEntityDataSource simply iterates through the iterator received via the constructor and retrieves the field values from the underlying entities. For this to work you must design the report using field names that correspond to the property IDs, so using the Store domain example from above the fields in a report showing the available items would have to be named 'name', 'is_active', 'category_code' etc. If you need to use a field that does not correspond to a property in the underlying entity, f.ex. when combining two fields into one you must override the getFieldValue() method and handle that special case there.

@Override
public Object getFieldValue(JRField jrField) {
  if (jrField.getName().equals("name_category_code") {
    Entity currentRecord = getCurrentEntity();
    return currentRecord.getString(Store.ITEM_NAME) + " - "
             + currentRecord.getAsString(Store.ITEM_CATEGORY_CODE);
  }
 
  return super.getFieldValue(jrField);
}

The way you view the report is just like in the jdbc report example above except you use the viewReport() method instead of viewJdbcReport().

Examples

documentation/manual.txt · Last modified: 2017/08/09 16:44 by darri