Skip to content

Commit

Permalink
Merge pull request #475 from goroskob/332_Initialize_annotation_for_i…
Browse files Browse the repository at this point in the history
…nitialize_ViewModel_method

Added @initialize annotation for ViewModel to define initialize method of any visibility #332
  • Loading branch information
manuel-mauky authored Apr 21, 2017
2 parents 9fb1c18 + e81771c commit 28d4402
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 40 deletions.
52 changes: 52 additions & 0 deletions mvvmfx/src/main/java/de/saxsys/mvvmfx/Initialize.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*******************************************************************************
* Copyright 2017 Gleb Koval
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package de.saxsys.mvvmfx;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
*
* This annotation is used to mark the method in a ViewModel to be called after all mvvmFx injections.
* If no method is marked, public <code>initialize()</code> method will be used, if present.<br>
* Example: <br>
* <br>
*
* <pre>
* public class SomeViewModel implements {@link ViewModel} {
*
* // mvvmFx injections
* {@literal @}{@link InjectScope}
* private SomeScope someScope;
* ...
*
* {@literal @}Initialize
* private void init() {
* someScope.subscribe(...);
* ...
* }
* }
* </pre>
*
* @author Gleb Koval
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Initialize {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package de.saxsys.mvvmfx.internal.viewloader;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
Expand Down Expand Up @@ -89,13 +90,13 @@ public static List<Field> getFieldsFromClassHierarchy(Class<?> type) {


/**
* Helper method to execute a callback on a given field. This method encapsulates the error handling logic and the
* handling of accessibility of the field.
* Helper method to execute a callback on a given member. This method encapsulates the error handling logic and the
* handling of accessibility of the member.
*
* After the callback is executed the accessibility of the field will be reset to the originally state.
* After the callback is executed the accessibility of the member will be reset to the originally state.
*
* @param field
* the field that is made accessible to run the callback
* @param member
* the member that is made accessible to run the callback
* @param callable
* the callback that will be executed.
* @param errorMessage
Expand All @@ -106,19 +107,19 @@ public static List<Field> getFieldsFromClassHierarchy(Class<?> type) {
* @throws IllegalStateException
* when something went wrong.
*/
public static <T> T accessField(final Field field, final Callable<T> callable, String errorMessage) {
public static <T> T accessMember(final AccessibleObject member, final Callable<T> callable, String errorMessage) {
if (callable == null) {
return null;
}
return AccessController.doPrivileged((PrivilegedAction<T>) () -> {
boolean wasAccessible = field.isAccessible();
boolean wasAccessible = member.isAccessible();
try {
field.setAccessible(true);
member.setAccessible(true);
return callable.call();
} catch (Exception exception) {
throw new IllegalStateException(errorMessage, exception);
} finally {
field.setAccessible(wasAccessible);
member.setAccessible(wasAccessible);
}
});
}
Expand All @@ -136,21 +137,21 @@ public static <T> T accessField(final Field field, final Callable<T> callable, S
* the new value that the field should be set to.
*/
public static void setField(final Field field, Object target, Object value) {
accessField(field, () -> field.set(target, value),
accessMember(field, () -> field.set(target, value),
"Cannot set the field [" + field.getName() + "] of instance [" + target + "] to value [" + value + "]");
}


/**
* Helper method to execute a callback on a given field. This method encapsulates the error handling logic and the
* handling of accessibility of the field. The difference to
* {@link ReflectionUtils#accessField(Field, Callable, String)} is that this method takes a callback that doesn't
* Helper method to execute a callback on a given member. This method encapsulates the error handling logic and the
* handling of accessibility of the member. The difference to
* {@link ReflectionUtils#accessMember(AccessibleObject, Callable, String)} is that this method takes a callback that doesn't
* return anything but only creates a sideeffect.
*
* After the callback is executed the accessibility of the field will be reset to the originally state.
* After the callback is executed the accessibility of the member will be reset to the originally state.
*
* @param field
* the field that is made accessible to run the callback
* @param member
* the member that is made accessible to run the callback
* @param sideEffect
* the callback that will be executed.
* @param errorMessage
Expand All @@ -159,19 +160,19 @@ public static void setField(final Field field, Object target, Object value) {
* @throws IllegalStateException
* when something went wrong.
*/
public static void accessField(final Field field, final SideEffect sideEffect, String errorMessage) {
public static void accessMember(final AccessibleObject member, final SideEffect sideEffect, String errorMessage) {
if (sideEffect == null) {
return;
}
AccessController.doPrivileged((PrivilegedAction) () -> {
boolean wasAccessible = field.isAccessible();
AccessController.doPrivileged((PrivilegedAction<?>) () -> {
boolean wasAccessible = member.isAccessible();
try {
field.setAccessible(true);
member.setAccessible(true);
sideEffect.call();
} catch (Exception exception) {
throw new IllegalStateException(errorMessage, exception);
} finally {
field.setAccessible(wasAccessible);
member.setAccessible(wasAccessible);
}
return null;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package de.saxsys.mvvmfx.internal.viewloader;

import de.saxsys.mvvmfx.Context;
import de.saxsys.mvvmfx.Initialize;
import de.saxsys.mvvmfx.InjectContext;
import de.saxsys.mvvmfx.InjectScope;
import de.saxsys.mvvmfx.InjectViewModel;
Expand All @@ -30,7 +31,6 @@
import javax.annotation.PostConstruct;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
Expand Down Expand Up @@ -166,7 +166,7 @@ public static <ViewType extends View<? extends ViewModelType>, ViewModelType ext
Optional<Field> fieldOptional = getViewModelField(view.getClass(), viewModelType);
if (fieldOptional.isPresent()) {
Field field = fieldOptional.get();
return ReflectionUtils.accessField(field, () -> (ViewModelType) field.get(view),
return ReflectionUtils.accessMember(field, () -> (ViewModelType) field.get(view),
"Can't get the viewModel of type <" + viewModelType + ">");
} else {
return null;
Expand All @@ -189,7 +189,7 @@ public static void injectViewModel(final View view, ViewModel viewModel) {
final Optional<Field> fieldOptional = getViewModelField(view.getClass(), viewModel.getClass());
if (fieldOptional.isPresent()) {
Field field = fieldOptional.get();
ReflectionUtils.accessField(field, () -> {
ReflectionUtils.accessMember(field, () -> {
Object existingViewModel = field.get(view);
if (existingViewModel == null) {
field.set(view, viewModel);
Expand Down Expand Up @@ -258,7 +258,7 @@ public static <V extends View<? extends VM>, VM extends ViewModel> void createAn
if (fieldOptional.isPresent()) {
Field field = fieldOptional.get();

ReflectionUtils.accessField(field, () -> {
ReflectionUtils.accessMember(field, () -> {
Object existingViewModel = field.get(view);

if (existingViewModel == null) {
Expand Down Expand Up @@ -294,7 +294,7 @@ static void createAndInjectScopes(Object viewModel, ContextImpl context) {
List<Field> scopeFields = getScopeFields(viewModel.getClass());

scopeFields.forEach(scopeField -> {
ReflectionUtils.accessField(scopeField, () -> injectScopeIntoField(scopeField, viewModel, context),
ReflectionUtils.accessMember(scopeField, () -> injectScopeIntoField(scopeField, viewModel, context),
"Can't inject Scope into ViewModel <" + viewModel.getClass() + ">");
});
}
Expand All @@ -305,7 +305,7 @@ public static void injectContext(View codeBehind, ContextImpl context) {

if (contextField.isPresent()) {
Field field = contextField.get();
ReflectionUtils.accessField(field, () -> {
ReflectionUtils.accessMember(field, () -> {
field.set(codeBehind, context);
}, "Can't inject Context into the view <" + codeBehind + ">");
}
Expand Down Expand Up @@ -372,10 +372,10 @@ public static <ViewType extends View<? extends ViewModelType>, ViewModelType ext
}

/**
* If a ViewModel has a method with the signature
* <code>public void initialize()</code> it will be invoked. If no such
* method is available nothing happens.
*
* If a ViewModel has a method annotated with {@link Initialize}
* or method with the signature <code>public void initialize()</code>
* it will be invoked. If no such method is available nothing happens.
*
* @param viewModel
* the viewModel that's initialize method (if available) will be
* invoked.
Expand All @@ -387,7 +387,9 @@ public static <ViewModelType extends ViewModel> void initializeViewModel(ViewMod
return;
}
try {
final Method initMethod = viewModel.getClass().getMethod("initialize");
Method annotatedMethod = getInitializeMethod(viewModel);
// find method annotated with @Initialize or use initialize() otherwise
final Method initMethod = annotatedMethod != null ? annotatedMethod : viewModel.getClass().getMethod("initialize");
// if there is a @PostConstruct annotation, throw an exception to prevent double injection
if(initMethod.isAnnotationPresent(PostConstruct.class)) {
throw new IllegalStateException(String.format("initialize method of ViewModel [%s] is annotated with @PostConstruct. " +
Expand All @@ -397,19 +399,21 @@ public static <ViewModelType extends ViewModel> void initializeViewModel(ViewMod
"https://github.com/sialcasa/mvvmFX/wiki/Dependency-Injection#lifecycle-postconstruct", viewModel));
}

AccessController.doPrivileged((PrivilegedAction) () -> {
try {
return initMethod.invoke(viewModel);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new IllegalStateException(
"mvvmFX wasn't able to call the initialize method of ViewModel [" + viewModel + "].", e);
}
});
ReflectionUtils.accessMember(initMethod, () -> initMethod.invoke(viewModel), "mvvmFX wasn't able to call the initialize method of ViewModel [" + viewModel + "].");
} catch (NoSuchMethodException e) {
// it's perfectly fine that a ViewModel has no initialize method.
}
}

private static <ViewModelType extends ViewModel> Method getInitializeMethod(ViewModelType viewModel) {
for (Method method : viewModel.getClass().getDeclaredMethods()) {
if(method.isAnnotationPresent(Initialize.class)) {
return method;
}
}
return null;
}

/**
* This method adds listeners for the {@link SceneLifecycle}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,4 +470,20 @@ public void testExistingViewModelWithoutInjectionInView() {

assertThat(TestViewModel.wasInitialized).isFalse();
}

/**
* Method annotated with {@link de.saxsys.mvvmfx.Initialize} annotation initializes the ViewModel
* */
@Test
public void testViewModelIsInitializedWithAnnotatatedMethod() {
TestViewModelWithAnnotatedInitialize.wasInitialized = false;

ViewTuple<TestFxmlViewWithViewModelWithAnnotatedInitialize, TestViewModelWithAnnotatedInitialize> tuple
= FluentViewLoader.fxmlView(TestFxmlViewWithViewModelWithAnnotatedInitialize.class).load();

TestViewModelWithAnnotatedInitialize viewModel = tuple.getViewModel();

assertThat(TestViewModelWithAnnotatedInitialize.wasInitialized).isTrue();

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright 2017 Gleb Koval
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package de.saxsys.mvvmfx.internal.viewloader.example;

import de.saxsys.mvvmfx.FxmlView;
import de.saxsys.mvvmfx.InjectViewModel;

/**
* This class is used as example View class that uses ViewModel initialized with
* method annotated with {@link de.saxsys.mvvmfx.Initialize} annotation
*
* @author Gleb Koval
*/
public class TestFxmlViewWithViewModelWithAnnotatedInitialize implements FxmlView<TestViewModelWithAnnotatedInitialize> {

@InjectViewModel
private TestViewModelWithAnnotatedInitialize viewModel;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*******************************************************************************
* Copyright 2017 Gleb Koval
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package de.saxsys.mvvmfx.internal.viewloader.example;

import de.saxsys.mvvmfx.Initialize;
import de.saxsys.mvvmfx.ViewModel;

/**
* This class is used as example ViewModel class that uses init method annotated with {@link Initialize}
*
* @author Gleb Koval
*/
public class TestViewModelWithAnnotatedInitialize implements ViewModel {

public static boolean wasInitialized = false;

@Initialize
private void init() {
wasInitialized = true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml" fx:controller="de.saxsys.mvvmfx.internal.viewloader.example.TestFxmlViewWithViewModelWithAnnotatedInitialize">

</VBox>

0 comments on commit 28d4402

Please sign in to comment.