I am currently developing some custom JavaFX components in my application and stumbled upon an issue with the binding.
The following is a component which allows to rate an item with one to five stars (Rating is an enum with the possible ratings).
public final class RatingControl extends HBox {
private final ReadOnlyObjectWrapper<Rating> ratingProperty = new ReadOnlyObjectWrapper<>( Rating.NO_RATING );
public RatingControl( final Rating initialRating ) {
createVisibleStars( );
ratingProperty.set( initialRating );
ratingProperty.addListener( ( observable, oldValue, newValue ) -> updateVisibleStars( newValue ) );
updateVisibleStars( ratingProperty.get( ) );
}
private void createVisibleStars( ) {
// omitted
}
private void onStarClicked( final Rating rating ) {
if ( ratingProperty.get( ) == rating ) {
setRating( Rating.NO_RATING );
} else {
setRating( rating );
}
}
private void updateVisibleStars( final Rating newValue ) {
// omitted
}
public ReadOnlyObjectProperty<Rating> ratingProperty( ) {
return ratingProperty.getReadOnlyProperty( );
}
public void setRating( final Rating rating ) {
ratingProperty.set( rating );
}
}
So my idea is as following: A click on a star changes the internal state (ratingProperty) by calling setRating. This can also be triggered from the outside. An update of this property changes the visual representation and makes sure that the visible rating is updated as well. This, again, can also be read readonly from the outside (via ratingProperty( )).
This works, but I am not happy with it. The calling component has to do something like the following (assuming that model has a Property named rating):
final RatingControl ratingControl = new RatingControl( model.rating( ).getValue( ) );
ratingControl.ratingProperty( ).addListener( ( observable, oldValue, newValue ) -> model.rating( ).set( newValue ) );
Using "normal" JavaFX components one would assume that a binding like the following is possible:
final RatingControl ratingControl = new RatingControl( );
ratingControl.ratingProperty( ).bindBidirectional( model.rating( ) );
final RatingControl ratingControl = new RatingControl( );
ratingControl.ratingProperty( ).bind( model.rating( ) );
So I wonder how I would achieve this. If I simply allow access to my internal property, I would not be able to set it when clicking a star, as a bound property cannot be set ("A bound value cannot be set).
After thinking about this a little bit more, I realized that it is not the bindings per se that are a problem, but only the unidirectional binding, as this leads to a contradiction when changing the value from within the component.
I tested the same structure with a StringProperty and a default TextField and noticed something. When using a bidirectional binding between the property and the field's textProperty, everything works as expected. However, when using a unidirectional binding, the text field no longer allows the user to change the input (this can actually be seen in TextInputControl.replaceText).
So I think it is not a problem to expose the internal property for the rating in my case. I just have to make sure that my onStarClicked method checks whether the property is bound or not.
private final ObjectProperty<Rating> ratingProperty = new SimpleObjectProperty<>( Rating.NO_RATING );
...
private void onStarClicked( final Rating rating ) {
if ( !ratingProperty.isBound( ) ) {
if ( ratingProperty.get( ) == rating ) {
setRating( Rating.NO_RATING );
} else {
setRating( rating );
}
}
}
public ObjectProperty<Rating> ratingProperty( ) {
return ratingProperty;
}
This allows to bind a property both uni- and bidirectional without leading to an exception in any case.
Related
This answer describes how to implement a custom accessor naming strategy in Mapstruct. In this example, the naming strategy is presumably applied to all mappers in the app.
Is there a way to only configure 1 mapper to use a custom naming strategy, and keep it default in all remaining mappers?
This is currently not possible if you restrict it to using it for one Mapper. However it does not make sense to restrict it based on a Mapper, what if you want to map from an Immutable data structure to a java bean data structure. Usually the package can be used to identify if you want to use strategy A or strategy B for this class.
You can write your own custom naming strategy that switches between these strategies depending on which package/class the methods are in.
See below for an example of switching between a custom strategy and the default strategy:
public class CustomAccessorNamingStrategy extends DefaultAccessorNamingStrategy {
private static final String CUSTOM_PACKAGE = "custom"; // custom package name.
private Elements elementUtils;
#Override
public void init(MapStructProcessingEnvironment processingEnvironment) {
super.init( processingEnvironment );
elementUtils = processingEnvironment.getElementUtils();
}
#Override
public boolean isGetterMethod(ExecutableElement method) {
if ( isCustomPackage( method ) ) {
// handle custom route and return.
} else { // fallback to default
return super.isGetterMethod( method );
}
}
#Override
public boolean isSetterMethod(ExecutableElement method) {
if ( isCustomPackage( method ) ) {
// handle custom route and return.
} else { // fallback to default
return super.isSetterMethod( method );
}
}
#Override
public String getPropertyName(ExecutableElement getterOrSetterMethod) {
if ( isCustomPackage( method ) ) {
// handle custom route and return.
} else { // fallback to default
return super.getPropertyName( getterOrSetterMethod );
}
}
private boolean isCustomPackage(ExecutableElement method) {
return getPackage( method ).contains( "." + CUSTOM_PACKAGE + "." ) // subpackage
|| getPackage( method ).endsWith( "." + CUSTOM_PACKAGE ); // current package
}
private String getPackage(ExecutableElement element) {
return elementUtils.getPackageOf( element ).getQualifiedName().toString();
}
}
for more information also see https://github.com/mapstruct/mapstruct/issues/2957 (which was created after this question got asked)
I'm attempting to implement a LiveData reference to data collected via gps and bluetooth, that is also grouped by a foreign key. I don't understand why the Transformations.switchMap doesn't trigger once I create a new foreignkey.
I've moved both, the observer and foreignkey creation around, the LiveData always returns null.
Both, the Database and the Repository are Singletons.
Activity.java
public class Activity extends AppCompatActivity {
...
private ViewModel mViewModel;
#Override
onCreate {
...
mViewModel = ViewModelProviders.of( this ).get( ViewModel.class );
init();
}
private class ObserverManager {
private List<Observer> observers = new ArrayList<>();
private List<LiveData> livedata = new ArrayList<>();
public void registerObserver( TextView view, int hid, int uid ) {
Observer observer = null;
LiveData ld = null;
ld = mViewModel.getLatestData();
observer = ( Observer<Float> ) ( #Nullable final Float data ) -> {
// String formatting...
};
observers.add( observer );
livedata.add( ld );
ld.observeForever( observer );
}
public void logValue( int index ) {
Log.d( "OBSERVERMANAGER", String.valueOf( livedata.get( index ).getValue() ) );
}
}
final ObserverManager mObserverManager = new ObserverManager();
}
During init() The foreignkey is inserted and updated, then the observer is attached dynamically.
The service logs the correct foreignkey and inserts values to eData entity, but the Transformations.swapMap never updates, or shows a value other than null.
ViewModel.java
...
private LiveData<Integer> mLiveActivityId;
private LiveData<Float> mLatestData;
ViewModel( Application application ) {
...
mLiveActivityId = mRepository.getLiveActivityId();
mLatestData = mRepository.getLatestData();
}
public LiveData<Float> getLatestData() {
return mLatestData;
}
Repository.java
...
private LiveData<Integer> mLiveActivityId;
private LiveData<Float> mLatestData;
Repository( Application application ) {
...
mLiveActivityId = mDataDao.getLiveActivityId();
mLatestData = Transformations.switchMap( mLiveActivityId, aid -> mDataDao.getLatestData( aid, 0 ) );
}
...
LiveData<Float> getSpeedGPSLatest() {
return mSpeedGPSLatest;
}
DataDao.java
#Transaction
#Query( "SELECT id FROM eActivity WHERE active = 1" )
LiveData<Integer> getLiveActivityId();
#Transaction
#Query( "SELECT data FROM eData WHERE aid = :aid AND source = :source AND time = " +
"( SELECT MAX(time) time FROM eData WHERE aid = :aid AND source = :source )" )
LiveData<Float> getLatestData( int aid, int source );
Is it even possible apply Transformations in the repository? So far I have only seen examples with them applied in the ViewModel. Unfortunately, due to some data sharing the same entity with a type field, that would mean I have to pass the LiveData objects back from a function in the repository, which I thought is wrong.
I have also read that switchMap creates a new LiveData object, would this somehow affect how ViewModel cannot read the new object in the repository?
If more code is required to understand the problem, just ask. Am totally stumped on this.
Simply put, yes you can. The Transformations.switchMap() was not the issue with LiveData not updating. Still cannot figure out how to communicate to the room db from a service, ie the follow up question Insert to room in service not updating LiveData in activity, but also solved that by doing things differently.
Using the Vaadin 8 #PropertyId annotation with the Binder::bindInstanceFields is certainly shorter and sweeter than writing a line of code for each field-property binding.
Person person; // `name` is String, `yearOfBirth` is Integer.
…
#PropertyId ( "name" )
final TextField nameField = new TextField ( "Full name:" ); // Bean property.
#PropertyId ( "yearOfBirth" )
final TextField yearOfBirthField = new TextField ( "Year of Birth:" ); // Bean property.
…
// Binding
Binder < Person > binder = new Binder <> ( Person.class );
binder.bindInstanceFields ( this );
binder.setBean ( person );
But we get an Exception thrown because the yearOfBirth property is an Integer, and this easy-does-it binding approach lacks an converter.
SEVERE:
java.lang.IllegalStateException: Property type 'java.lang.Integer' doesn't match the field type 'java.lang.String'. Binding should be configured manually using converter.
Does that mean Binder::bindInstanceFields can be used only a beans made entirely of properties of String data type?
Is there a way to specify a Converter such as StringToIntegerConverter without having to itemize each and every binding in code?
See Vaadin Framework, Vaadin Data Model, Binding Data to Forms:
Conversions
You can also bind application data to a UI field component even though the types do not match.
Binder#bindInstanceFields() says:
It's not always possible to bind a field to a property because their types are incompatible. E.g. custom converter is required to bind HasValue<String> and Integer property (that would be a case of "age" property). In such case IllegalStateException will be thrown unless the field has been configured manually before calling the bindInstanceFields(Object) method.
[...]: the bindInstanceFields(Object) method doesn't override existing bindings.
[Emphases by me.]
So, AFAIU, this should work:
private final TextField siblingsCount = new TextField( "№ of Siblings" );
...
binder.forField( siblingsCount )
.withNullRepresentation( "" )
.withConverter(
new StringToIntegerConverter( Integer.valueOf( 0 ), "integers only" ) )
.bind( Child::getSiblingsCount, Child::setSiblingsCount );
binder.bindInstanceFields( this );
But it still throws:
java.lang.IllegalStateException: Property type 'java.lang.Integer' doesn't match the field type 'java.lang.String'. Binding should be configured manually using converter.
...
at com.vaadin.data.Binder.bindInstanceFields(Binder.java:2135)
...
Are you kidding me? That's what I did, didn't I? I rather doubt about "doesn't override existing bindings". Or, if not actually overridden, it seems they are ignored in bindInstanceFields(), at least.
The same manual binding configuration works when not using Binder#bindInstanceFields() but the approach with individual bindings for each field.
See also the thread Binding from Integer not working in the Vaadin Framework Data Binding forum and issue #8858 Binder.bindInstanceFields() overwrites existing bindings.
Workaround
Less convoluted than #cfrick's answer:
/** Used for workaround for Vaadin issue #8858
* 'Binder.bindInstanceFields() overwrites existing bindings'
* https://github.com/vaadin/framework/issues/8858
*/
private final Map<String, Component> manualBoundComponents = new HashMap<>();
...
// Commented here and declared local below for workaround for Vaadin issue #8858
//private final TextField siblingsCount = new TextField( "№ of Siblings" );
...
public ChildView() {
...
// Workaround for Vaadin issue #8858
// Declared local here to prevent processing by Binder#bindInstanceFields()
final TextField siblingsCount = new TextField( "№ of Siblings" );
manualBoundComponents.put( "siblingsCount", siblingsCount );
binder.forField( siblingsCount )
.withNullRepresentation( "" )
.withConverter( new StringToIntegerConverter( Integer.valueOf( 0 ), "integers only" ) )
.bind( Child::getSiblingsCount, Child::setSiblingsCount );
binder.bindInstanceFields( this );
...
// Workaround for Vaadin issue #8858
addComponent( manualBoundComponents.get( "siblingsCount" ) );
//addComponent( siblingsCount );
...
}
UPDATE
Fix #8998 Make bindInstanceFields not bind fields already bound using functions.
The source code for that fix appears at least in Vaadin 8.1.0 alpha 4 pre-release (and perhaps others).
Update by Basil Bourque…
Your idea, shown above, to use Binder::bindInstanceFields after a manual binding for the non-compatible (Integer) property does indeed seem to be working for me. You complained that in your experimental code the call to Binder::bindInstanceFields failed to follow the documented behavior where the call “doesn't override existing bindings”.
But it seems to work for me. Here is an example app for Vaadin 8.1.0 alpha 3. First I manually bind yearOfBirth property. Then I use binder.bindInstanceFields to bind the #PropertyId annotated name property. The field for both properties appear populated and respond to user-edits.
Did I miss something or is this working properly as documented? If I made a mistake, please delete this section.
package com.example.vaadin.ex_formatinteger;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.data.Binder;
import com.vaadin.data.converter.StringToIntegerConverter;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.*;
import javax.servlet.annotation.WebServlet;
/**
* This UI is the application entry point. A UI may either represent a browser window
* (or tab) or some part of a html page where a Vaadin application is embedded.
* <p>
* The UI is initialized using {#link #init(VaadinRequest)}. This method is intended to be
* overridden to add component to the user interface and initialize non-component functionality.
*/
#Theme ( "mytheme" )
public class MyUI extends UI {
Person person;
//#PropertyId ( "honorific" )
final TextField honorific = new TextField ( "Honorific:" ); // Bean property.
//#PropertyId ( "name" )
final TextField name = new TextField ( "Full name:" ); // Bean property.
// Manually bind property to field.
final TextField yearOfBirthField = new TextField ( "Year of Birth:" ); // Bean property.
final Label spillTheBeanLabel = new Label ( ); // Debug. Not a property.
#Override
protected void init ( VaadinRequest vaadinRequest ) {
this.person = new Person ( "Ms.", "Margaret Hamilton", Integer.valueOf ( 1936 ) );
Button button = new Button ( "Spill" );
button.addClickListener ( ( Button.ClickEvent e ) -> {
spillTheBeanLabel.setValue ( person.toString ( ) );
} );
// Binding
Binder < Person > binder = new Binder <> ( Person.class );
binder.forField ( this.yearOfBirthField )
.withNullRepresentation ( "" )
.withConverter ( new StringToIntegerConverter ( Integer.valueOf ( 0 ), "integers only" ) )
.bind ( Person:: getYearOfBirth, Person:: setYearOfBirth );
binder.bindInstanceFields ( this );
binder.setBean ( person );
setContent ( new VerticalLayout ( honorific, name, yearOfBirthField, button, spillTheBeanLabel ) );
}
#WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )
#VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )
public static class MyUIServlet extends VaadinServlet {
}
}
And simple Person class.
package com.example.vaadin.ex_formatinteger;
import java.time.LocalDate;
import java.time.ZoneId;
/**
* Created by Basil Bourque on 2017-03-31.
*/
public class Person {
private String honorific ;
private String name;
private Integer yearOfBirth;
// Constructor
public Person ( String honorificArg , String nameArg , Integer yearOfBirthArg ) {
this.honorific = honorificArg;
this.name = nameArg;
this.yearOfBirth = yearOfBirthArg;
}
public String getHonorific ( ) {
return honorific;
}
public void setHonorific ( String honorific ) {
this.honorific = honorific;
}
// name property
public String getName ( ) {
return name;
}
public void setName ( String nameArg ) {
this.name = nameArg;
}
// yearOfBirth property
public Integer getYearOfBirth ( ) {
return yearOfBirth;
}
public void setYearOfBirth ( Integer yearOfBirth ) {
this.yearOfBirth = yearOfBirth;
}
// age property. Calculated, so getter only, no setter.
public Integer getAge ( ) {
int age = ( LocalDate.now ( ZoneId.systemDefault ( ) )
.getYear ( ) - this.yearOfBirth );
return age;
}
#Override
public String toString ( ) {
return "Person{ " +
"honorific='" + this.getHonorific () + '\'' +
", name='" + this.getName () +
", yearOfBirth=" + this.yearOfBirth +
", age=" + this.getAge () +
" }";
}
}
So far the best way to handle this for me, is to write a dedicated field for
the type of input (note that this "pattern" also works for writing composite
fields).
See the complete example here. The IntegerField is the field to wrap the Integer
yet in another binder via a bean to hold the actual value.
// run with `spring run --watch <file>.groovy`
#Grab('com.vaadin:vaadin-spring-boot-starter:2.0.1')
import com.vaadin.ui.*
import com.vaadin.annotations.*
import com.vaadin.shared.*
import com.vaadin.data.*
import com.vaadin.data.converter.*
class IntegerField extends CustomField<Integer> {
final Binder<Bean> binder
final wrappedField = new TextField()
IntegerField() {
binder = new BeanValidationBinder<IntegerField.Bean>(IntegerField.Bean)
binder.forField(wrappedField)
.withNullRepresentation('')
.withConverter(new StringToIntegerConverter("Only numbers"))
.bind('value')
doSetValue(null)
}
IntegerField(String caption) {
this()
setCaption(caption)
}
Class<Integer> getType() {
Integer
}
com.vaadin.ui.Component initContent() {
wrappedField
}
Registration addValueChangeListener(HasValue.ValueChangeListener<Integer> listener) {
binder.addValueChangeListener(listener)
}
protected void doSetValue(Integer value) {
binder.bean = new IntegerField.Bean(value)
}
Integer getValue() {
binder.bean?.value
}
#groovy.transform.Canonical
static class Bean {
Integer value
}
}
#groovy.transform.Canonical
class Person {
#javax.validation.constraints.Min(value=18l)
Integer age
}
class PersonForm extends FormLayout {
#PropertyId('age')
IntegerField ageField = new IntegerField("Age")
PersonForm() {
addComponents(ageField)
}
}
#com.vaadin.spring.annotation.SpringUI
#com.vaadin.annotations.Theme("valo")
class MyUI extends UI {
protected void init(com.vaadin.server.VaadinRequest request) {
def form = new PersonForm()
def binder = new BeanValidationBinder<Person>(Person)
binder.bindInstanceFields(form)
binder.bean = new Person()
content = new VerticalLayout(
form,
new Button("Submit", {
Notification.show(binder.bean.toString())
} as Button.ClickListener)
)
}
}
the problem still exists in Vaadin 8.4.0, Converter is not recognized and keep throwing the IllegalStateException.
But there is a simple workaround to this horrible bug:
binder.bind(id, obj -> obj.getId() + "", null); //the ValueProvider "getter" could consider that getId returns null
I need help writing a Drools rule. I have two classes named Context and CreditReport.
Context is inserted as a fact into the knowledge session before the rules are fired.
I need to write a rule that prints 'Excellent' on the console when the Credit Score is more than 800.
Ideally, I'd insert CreditReport directly into the session, but unfortunately I do not have that option.
The rule that I've written doesn't look good as:
The then part has an if statement
I am type-casting Object to CreditReport
Thanks for your help!
// Context.java
public class Context {
private Map<String, Object> data = Maps.newHashMap();
public <T> T getData(final String key, final Class<T> clazz) {
return clazz.cast(data.get(key));
}
public void putData(final String key, final Object value) {
this.data.put(key, value);
}
}
// CreditReport.java
public class CreditReport {
private final String name;
private final int creditScore;
public String getName() {
return this.name;
}
public int getCreditScore() {
return this.creditScore;
}
}
// Main method
context.put("creditReport", new CreditReport("John", 810));
session.insert(Arrays.asList(context));
session.fireAllRules();
// Rule
rule "Excellent_Score"
when Context($creditReportObject : getData("creditReport"))
then
final CreditReport creditReport = (CreditReport) $creditReportObject;
if (creditReport.getCreditScore() >= 800) {
System.out.println("Excellent");
}
end
What makes you insert a List<Context> containing a single Context object? The Java code should do
context.put("creditReport", new CreditReport("John", 810));
session.insert( context );
session.fireAllRules();
The rule can now be written as
rule "Excellent_Score"
when
Context( $creditReportObject : getData("creditReport") )
CreditReport( creditScore > 800 ) from $creditReportObject
then
System.out.println("Excellent");
end
You could, of course, get and insert the CreditReport from Context. - I suspect it's more convoluted that what you've shown, but "I do not have that option" is a code smell anyway.
Edit A single rule for more than one reason for printing "excellent" could be written like the one below, although this isn't much better that two rules, considering that you can wrap the RHS into a method or DRL function.
rule "Excellent_Score_2"
when
Context( $creditReport : getData("creditReport"),
$account: getData("account") )
( CreditReport( creditScore > 800 ) from $creditReport
or
Account( balance >= 5000 ) from $account )
then
System.out.println("Excellent");
end
I am writing a java simulation application which has a lot of entities to simulate. Each of these entities has a certain state at any time in the system. A possible and natural approach to model such an entity would be using the state (or state machine) pattern. The problem is that it creates a lot of objects during the runtime if there are a lot of state switches, what might cause bad system performance. What design alternatives do I have? I want performance to be the main criteria after maintainability.
Thanks
The below code will give you high performance (~10ns/event) zero runtime GC state machine implementation. Use explicit state machines whenever you have a concept of state in the system or component, this not only makes the code clean and scalable but also lets people (not even programmers) see immediately what the system does without having to dig in numerous callbacks:
abstract class Machine {
enum State {
ERROR,
INITIAL,
STATE_0,
STATE_1,
STATE_2;
}
enum Event {
EVENT_0,
EVENT_1,
EVENT_2;
}
public static final int[][] fsm;
static {
fsm = new int[State.values().length][];
for (State s: State.values()) {
fsm[s.ordinal()] = new int[Event.values().length];
}
}
protected State state = State.INITIAL;
// child class constructor example
// public Machine() {
// // specify allowed transitions
// fsm[State.INITIAL.ordinal()][Event.EVENT_0.ordinal()] = State.STATE_0.ordinal();
// fsm[State.STATE_0.ordinal()][Event.EVENT_0.ordinal()] = State.STATE_0.ordinal();
// fsm[State.STATE_0.ordinal()][Event.EVENT_1.ordinal()] = State.STATE_1.ordinal();
// fsm[State.STATE_1.ordinal()][Event.EVENT_1.ordinal()] = State.STATE_1.ordinal();
// fsm[State.STATE_1.ordinal()][Event.EVENT_2.ordinal()] = State.STATE_2.ordinal();
// fsm[State.STATE_1.ordinal()][Event.EVENT_0.ordinal()] = State.STATE_0.ordinal();
// fsm[State.STATE_2.ordinal()][Event.EVENT_2.ordinal()] = State.STATE_2.ordinal();
// fsm[State.STATE_2.ordinal()][Event.EVENT_1.ordinal()] = State.STATE_1.ordinal();
// fsm[State.STATE_2.ordinal()][Event.EVENT_0.ordinal()] = State.STATE_0.ordinal();
// }
public final void onEvent(Event event) {
final State next = State.values()[ fsm[state.ordinal()][event.ordinal()] ];
if (next == State.ERROR) throw new RuntimeException("invalid state transition");
if (acceptEvent(event)) {
final State prev = state;
state = next;
handleEvent(prev, event);
}
}
public abstract boolean acceptEvent(Event event);
public abstract void handleEvent(State prev, Event event);
}
if fsm is replaced with a unidimentional array of size S*E it will also improve cache proximity characteristics of the state machine.
My suggestion:
Have you "transitions managment" be configurable (i.e - via XML).
Load the XML to a repository holding the states.
The internal data structure will be a Map:
Map<String,Map<String,Pair<String,StateChangeHandler>>> transitions;
The reason for my selection is that this will be a map from a state name
To a map of "inputs" and new states:
Each map defines a map between possible input and the new state it leads to which is defined by the state name and a StateChangeHandler I will elaborate on later
change state method at the repository would have a signature of:
void changeState(StateOwner owner, String input)
This way the repository is stateless in the sense of the state owner using it, you can copy one copy, and not worry about thread safety issues.
StateOwner will be an interface your Classes that need state changing should implement.
I think the interface should look like this:
public interace StateOwner {
String getState();
void String setState(String newState);
}
In addition, you will have a ChangeStateHandler interface:
public interface StateChangeHandler {
void onChangeState(StateOwner, String newState) {
}
}
When the repository's changeState method is called, it will
check at the data structure that the current state of the stateOwner has a map of "inputs".
If it has such a map, it will check if the input has a new State to change to, and invoke the onChangeState method.
I will suggest you have a default implementation of the StateChangeHandler, and of course sub classes that will define the state change behavior more explicitly.
As I previously mentioned, all this can be loaded from an XML configuration, and using reflection you can instantitate StateChangeHandler objects based on their name (as mentioned at the XML) and that will be held in the repository.
Efficiency and good performance rely and obtained using the following points:
a. The repository itself is stateless - no internal references of StateOwner should be kept.
b. You load the XML once , when the system starts, after that you should work with in memory data structure.
c. You will provide specific StateChangeHandler implementation only when needed, the default implementation should do basicaly nothing.
d. No need to instantiate new objects of Handlers (as they should be stateless)
This proposal isn't universal, it isn't UML compliant but for simple thing, it's a simple mean.
import java.util.HashMap;
import java.util.Map;
class Mobile1
{
enum State {
FIRST, SECOND, THIRD
}
enum Event {
FIRST, SECOND, THIRD
}
public Mobile1() { // initialization may be done by loading a file
Map< Event, State > tr;
tr = new HashMap<>();
tr.put( Event.FIRST, State.SECOND );
_fsm.put( State.FIRST, tr );
tr = new HashMap<>();
tr.put( Event.SECOND, State.THIRD );
_fsm.put( State.SECOND, tr );
tr = new HashMap<>();
tr.put( Event.THIRD, State.FIRST );
_fsm.put( State.THIRD, tr );
}
public void activity() { // May be a long process, generating events,
System.err.println( _state );// to opposite to "action()" see below
}
public void handleEvent( Event event ) {
Map< Event, State > trs = _fsm.get( _state );
if( trs != null ) {
State futur = trs.get( event );
if( futur != null ) {
_state = futur;
// here we may call "action()" a small piece of code executed
// once per transition
}
}
}
private final Map<
State, Map<
Event, State >> _fsm = new HashMap<>();
private /* */ State _state = State.FIRST;
}
public class FSM_Test {
public static void main( String[] args ) {
Mobile1 m1 = new Mobile1();
m1.activity();
m1.handleEvent( Mobile1.Event.FIRST );
m1.activity();
m1.handleEvent( Mobile1.Event.SECOND );
m1.activity();
m1.handleEvent( Mobile1.Event.FIRST ); // Event not handled
m1.activity();
m1.handleEvent( Mobile1.Event.THIRD );
m1.activity();
}
}
output:
FIRST
SECOND
THIRD
THIRD
FIRST