/**
* Get more info at : www.jrebirth.org .
* Copyright JRebirth.org © 2011-2013
* Contact : sebastien.bordes@jrebirth.org
*
* 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 org.jrebirth.af.core.component.basic;
import static org.jrebirth.af.core.wave.Builders.wave;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jrebirth.af.api.annotation.AfterInit;
import org.jrebirth.af.api.annotation.BeforeInit;
import org.jrebirth.af.api.annotation.OnRelease;
import org.jrebirth.af.api.annotation.Releasable;
import org.jrebirth.af.api.annotation.SkipAnnotation;
import org.jrebirth.af.api.command.Command;
import org.jrebirth.af.api.command.CommandBean;
import org.jrebirth.af.api.component.basic.Component;
import org.jrebirth.af.api.component.basic.InnerComponent;
import org.jrebirth.af.api.component.behavior.Behavior;
import org.jrebirth.af.api.exception.CoreException;
import org.jrebirth.af.api.exception.CoreRuntimeException;
import org.jrebirth.af.api.exception.JRebirthThreadException;
import org.jrebirth.af.api.facade.JRebirthEventType;
import org.jrebirth.af.api.key.UniqueKey;
import org.jrebirth.af.api.link.Notifier;
import org.jrebirth.af.api.log.JRLogger;
import org.jrebirth.af.api.service.Service;
import org.jrebirth.af.api.ui.Model;
import org.jrebirth.af.api.wave.Wave;
import org.jrebirth.af.api.wave.Wave.Status;
import org.jrebirth.af.api.wave.WaveBean;
import org.jrebirth.af.api.wave.WaveGroup;
import org.jrebirth.af.api.wave.checker.WaveChecker;
import org.jrebirth.af.api.wave.contract.WaveData;
import org.jrebirth.af.api.wave.contract.WaveType;
import org.jrebirth.af.core.concurrent.AbstractJrbRunnable;
import org.jrebirth.af.core.concurrent.JRebirth;
import org.jrebirth.af.core.concurrent.JrbReferenceRunnable;
import org.jrebirth.af.core.link.AbstractReady;
import org.jrebirth.af.core.link.ComponentEnhancer;
import org.jrebirth.af.core.link.LinkMessages;
import org.jrebirth.af.core.log.JRLoggerFactory;
import org.jrebirth.af.core.util.CheckerUtility;
import org.jrebirth.af.core.util.ClassUtility;
import org.jrebirth.af.core.util.ObjectUtility;
/**
*
* The class <strong>AbstractComponent</strong>.
*
* This is the base class for all of each of JRebirth pattern subclasses.<br />
* It allow to send waves.
*
* All things related to wave management must be execute into the JRebirth Thread
*
* @author Sébastien Bordes
*
* @param <C> the class type of the subclass
*/
@SkipAnnotation(false)
public abstract class AbstractComponent<C extends Component<C>> extends AbstractReady<C> implements Component<C>, LinkMessages {
/** The default fallback wave handle method. */
public static final String PROCESS_WAVE_METHOD_NAME = "processWave";
/** The class logger. */
private static final JRLogger LOGGER = JRLoggerFactory.getLogger(AbstractComponent.class);
/** The return wave type map. */
// private final Map<WaveType, WaveType> returnWaveTypeMap = new HashMap<>();
/** A map that store all annotated methods to call sorted by lifecycle phase. */
private Map<String, List<Method>> lifecycleMethod;
/** The root component not null for inner component. */
protected Component<?> rootComponent;
/** The map that store inner models loaded. */
protected Map<InnerComponent<?>, Component<?>> innerComponentMap;
/** The list that store sorted inner models loaded. */
protected List<Component<?>> innerComponentList;
/**
* Short cut method used to retrieve the notifier.
*
* @return the notifier retrieved from global facade
*/
private Notifier getNotifier() {
return getLocalFacade().getGlobalFacade().getNotifier();
}
/**
* {@inheritDoc}
*/
@Override
public final void listen(final WaveType... waveTypes) {
// Call the other method with null waveChecker & method
listen(null, null, waveTypes);
}
/**
* {@inheritDoc}
*/
@Override
public final void listen(final WaveChecker waveChecker, final WaveType... waveTypes) {
// Call the other method with null method
listen(waveChecker, null, waveTypes);
}
/**
* Return the human-readable list of Wave Type.
*
* @param waveTypes the list of wave type
*
* @return the string list of Wave Type
*/
private String getWaveTypesString(final WaveType[] waveTypes) {
final StringBuilder sb = new StringBuilder();
for (final WaveType waveType : waveTypes) {
sb.append(waveType.toString()).append(" ");
}
return sb.toString();
}
/**
* {@inheritDoc}
*/
@Override
public final void listen(final WaveChecker waveChecker, final Method method, final WaveType... waveTypes) {
// Check API compliance
CheckerUtility.checkWaveTypeContract(this.getClass(), waveTypes);
final Component<?> waveReady = this;
LOGGER.trace(LinkMessages.LISTEN_WAVE_TYPE, getWaveTypesString(waveTypes), waveReady.getClass().getSimpleName());
// Use the JRebirth Thread to add new subscriptions for given Wave Type
JRebirth.runIntoJIT(new AbstractJrbRunnable(LISTEN_WAVE_TYPE.getText(getWaveTypesString(waveTypes), waveReady.getClass().getSimpleName())) {
@Override
public void runInto() throws JRebirthThreadException {
getNotifier().listen(waveReady, waveChecker, method, waveTypes);
}
});
}
// /**
// * {@inheritDoc}
// */
// @Override
// public final void registerCallback(final WaveType callType, final WaveType responseType) {
//
// // Call the generic method
// registerCallback(null, callType, responseType, null);
// }
// /**
// * {@inheritDoc}
// */
// @Override
// public final WaveType getReturnWaveType(final WaveType waveType) {
// return this.returnWaveTypeMap.get(waveType);
// }
/**
* {@inheritDoc}
*/
@Override
public final void unlisten(final WaveType... waveTypes) {
// Store an hard link to be able to use current class into the closure
final Component waveReady = this;
LOGGER.trace(LinkMessages.UNLISTEN_WAVE_TYPE, getWaveTypesString(waveTypes), waveReady.getClass().getSimpleName());
// Use the JRebirth Thread to manage Waves
JRebirth.runIntoJIT(new AbstractJrbRunnable(UNLISTEN_WAVE_TYPE.getText(getWaveTypesString(waveTypes), waveReady.getClass().getSimpleName())) {
@Override
protected void runInto() throws JRebirthThreadException {
getNotifier().unlisten(waveReady, waveTypes);
}
});
}
/**
* {@inheritDoc}
*/
@Override
public final void sendWave(final Wave wave) {
// Define the from class if it didn't been done before (manually)
if (wave.fromClass() == null) {
wave.fromClass(this.getClass());
}
sendWaveIntoJit(wave);
}
/**
* {@inheritDoc}
*/
@Override
public final <WB extends WaveBean> Wave sendWave(final WaveType waveType, final WB waveBean) {
return sendWaveIntoJit(createWave(WaveGroup.UNDEFINED, waveType, (Class<?>) null, waveBean));
}
/**
* {@inheritDoc}
*/
@Override
public final Wave sendWave(final WaveType waveType, final WaveData<?>... waveData) {
return sendWaveIntoJit(createWave(WaveGroup.UNDEFINED, waveType, null, waveData));
}
/**
* {@inheritDoc}
*/
@Override
public final <WB extends WaveBean> Wave callCommand(final Class<? extends CommandBean<WB>> commandClass, final WB waveBean) {
return sendWaveIntoJit(createWave(WaveGroup.CALL_COMMAND, null, commandClass, waveBean));
}
/**
* {@inheritDoc}
*/
@Override
public final Wave callCommand(final Class<? extends Command> commandClass, final WaveData<?>... data) {
return sendWaveIntoJit(createWave(WaveGroup.CALL_COMMAND, null, commandClass, data));
}
/**
* {@inheritDoc}
*/
@Override
public final Wave returnData(final Class<? extends Service> serviceClass, final WaveType waveType, final WaveData<?>... data) {
return sendWaveIntoJit(createWave(WaveGroup.RETURN_DATA, waveType, serviceClass, data));
}
/**
* {@inheritDoc}
*/
@Override
public final Wave attachUi(final Class<? extends Model> modelClass, final WaveData<?>... data) {
return sendWaveIntoJit(createWave(WaveGroup.ATTACH_UI, null, modelClass, data));
}
/**
* Send the given wave using the JRebirth Thread.
*
* @param wave the wave to send
*
* @return the wave sent to JIT (with Sent status)
*/
private Wave sendWaveIntoJit(final Wave wave) {
CheckerUtility.checkWave(wave);
wave.status(Status.Sent);
// Use the JRebirth Thread to manage Waves
JRebirth.runIntoJIT(new AbstractJrbRunnable(SEND_WAVE.getText(wave.toString())) {
@Override
public void runInto() throws JRebirthThreadException {
getNotifier().sendWave(wave);
}
});
return wave;
}
/**
* Build a wave object.
*
* @param waveGroup the group of the wave
* @param waveType the type of the wave
* @param componentClass the component class if any
* @param waveData wave data to use
*
* @return the wave built
*/
private Wave createWave(final WaveGroup waveGroup, final WaveType waveType, final Class<?> componentClass, final WaveData<?>... waveData) {
final Wave wave = wave()
.waveGroup(waveGroup)
.waveType(waveType)
.fromClass(this.getClass())
.componentClass(componentClass)
.addDatas(waveData);
// Track wave creation
getLocalFacade().getGlobalFacade().trackEvent(JRebirthEventType.CREATE_WAVE, this.getClass(), wave.getClass());
return wave;
}
/**
* Build a wave object with its dedicated WaveBean.
*
* @param waveGroup the group of the wave
* @param waveType the type of the wave
* @param componentClass the component class if any
* @param waveBean the wave bean that holds all required wave data
*
* @return the wave built
*/
private Wave createWave(final WaveGroup waveGroup, final WaveType waveType, final Class<?> componentClass, final WaveBean waveBean) {
final Wave wave = wave()
.waveGroup(waveGroup)
.waveType(waveType)
.fromClass(this.getClass())
.componentClass(componentClass)
.waveBean(waveBean);
// Track wave creation
getLocalFacade().getGlobalFacade().trackEvent(JRebirthEventType.CREATE_WAVE, this.getClass(), wave.getClass());
return wave;
}
// /**
// * This method is called before each execution of the command.
// *
// * It will parse the given wave to store local command properties.
// *
// * Wave parsing mechanism is composed by three steps:
// * <ol>
// * <li>Parse WaveBean properties and copy them into command ones if they exist (by reflection)</li>
// * <li>Parse WaveData keys and copy them into command ones if they exist (by reflection)</li>
// * <li>Call {@link #parseWave(Wave)} method for later customization</li>
// * </ol>
// *
// * @param wave the wave to parse
// */
// private void parseInternalWave(final Wave wave) {
//
// // Parse WaveBean
// // WB waveBean = getWaveBean(wave);
// // if (waveBean != null && !(waveBean instanceof DefaultWaveBean)) {
// // for (Field f : ClassUtility.retrievePropertyList(waveBean.getClass())) {
// // try {
// // tryToSetProperty(f.getName(), f.get(waveBean));
// // } catch (IllegalArgumentException | IllegalAccessException e) {
// // LOGGER.error("Fail to get field value " + f.getName() + " from " + waveBean.getClass(), e);
// // }
// // }
// // }
//
// // Parse WaveData
// for (final WaveData<?> wd : wave.getWaveItems()) {
// tryToSetProperty(wd.getKey().toString(), wd.getValue());
// }
//
// // Call customized method
// // parseWave(wave);
// }
//
// /**
// * Try to set the value of the given property for the current class.
// *
// * @param fieldName the field to initialize
// * @param fieldValue the field value to set
// */
// private void tryToSetProperty(final String fieldName, final Object fieldValue) {
// try {
// this.getClass().getField(fieldName).set(this, fieldValue);
// } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
// LOGGER.error("Fail to set value for field " + fieldName, e);
// }
// }
/**
* Customizable method used to perform more action before command execution.
*
* @param wave the given wave to parser before command execution
*/
// protected abstract void parseWave(final Wave wave);
// /**
// * {@inheritDoc}
// */
// @Override
// public final void handle(final Wave wave) throws WaveException {
// try {
// // Build parameter list of the searched method
// final List<Object> parameterValues = new ArrayList<>();
// for (final WaveData<?> wd : wave.getWaveItems()) {
// // Add only wave items defined as parameter
// if (wd.getKey().isParameter()) {
// parameterValues.add(wd.getValue());
// }
// }
// // Add the current wave to process
// parameterValues.add(wave);
//
// // Search the wave handler method
// final Method method = ClassUtility.getMethodByName(this.getClass(), ClassUtility.underscoreToCamelCase(wave.getWaveType().toString()));
// if (method != null) {
// // Call this method with right parameters
// method.invoke(this, parameterValues.toArray());
// }
// } catch (final NoSuchMethodException e) {
//
// LOGGER.info(CUSTOM_METHOD_NOT_FOUND, e.getMessage());
// // If no method was found, call the default method
// processWave(wave);
//
// } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
// LOGGER.error(WAVE_DISPATCH_ERROR, e);
// // Propagate the wave exception
// throw new WaveException(wave, e);
// }
// }
/**
* Process the wave. Typically by using a switch on the waveType.
*
* @param wave the wave received
*/
protected abstract void processWave(final Wave wave);
/**
* Private method used to grab the right WaveType<?> java type.
*
* @return the this instance with the right generic type
*/
private Component<?> getWaveReady() {
return this;
}
/**
* Call annotated methods corresponding at given lifecycle annotation.
*
* @param annotationClass the annotation related to the lifecycle
*/
private void callAnnotatedMethod(final Class<? extends Annotation> annotationClass) {
if (this.lifecycleMethod.get(annotationClass.getName()) != null) {
for (final Method method : this.lifecycleMethod.get(annotationClass.getName())) {
try {
ClassUtility.callMethod(method, this);
} catch (final CoreException e) {
LOGGER.error(CALL_ANNOTATED_METHOD_ERROR, e);
}
}
}
}
/**
* The component is now ready to do custom initialization tasks.
*
* @throws CoreException if an error occurred
*/
protected abstract void ready() throws CoreException;
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public void setup() throws CoreException {
final boolean canProcessAnnotation = ComponentEnhancer.canProcessAnnotation((Class<? extends Component<?>>) this.getClass());
if (canProcessAnnotation) {
// Search Singleton and Multiton annotation on field
ComponentEnhancer.injectComponent(this);
// Attach custom method configured with custom Lifecycle annotation
this.lifecycleMethod = ComponentEnhancer.defineLifecycleMethod(this);
// Search OnWave annotation to manage auto wave handler setup
ComponentEnhancer.manageOnWaveAnnotation(this);
}
callAnnotatedMethod(BeforeInit.class);
manageOptionalData();
if (canProcessAnnotation) {
ComponentEnhancer.injectInnerComponent(this);
}
// Initialize all inner components
initInternalInnerComponents();
// Prepare the current component
ready();
callAnnotatedMethod(AfterInit.class);
}
/**
*
*/
protected abstract void manageOptionalData();
// {
//
// for (final Object data : getKey().getOptionalData()) {
//
// if (data instanceof BehaviorData) {
//
// addBehavior((BehaviorData) data);
//
// } else if (data instanceof Class && ((Class<?>) data).isAssignableFrom(Behavior.class)) {
//
// addBehavior((Class<Behavior<BehaviorData>>) data);
//
// }
//
// }
//
// }
/**
* {@inheritDoc}
*/
@Override
public boolean release() {
boolean released = false;
// Check if some method annotated by Releasable annotation are available
if (ObjectUtility.checkAllMethodReturnTrue(this, ClassUtility.getAnnotatedMethods(this.getClass(), Releasable.class))) {
// Release the component and its internal components into JIT
JRebirth.runIntoJIT(new JrbReferenceRunnable("Release " + this.getClass().getCanonicalName(), this::internalRelease));
released = true;
}
return released;
}
/**
* Perform the internal release.
*/
private void internalRelease() {
// try {
// setKey(null);
// getNotifier().unlistenAll(getWaveReady());
callAnnotatedMethod(OnRelease.class);
if (getLocalFacade() != null) {
getLocalFacade().unregister(getKey());
}
// thisObject.ready = false;
// Shall also release all InnerComponent
if (getInnerComponentList().isPresent()) {
final List<Component<?>> toRemove = new ArrayList<>();
for (final Component<?> innerComponent : getInnerComponentList().get()) {
// Release the InnerComponent
innerComponent.release();
// Store it to avoid co-modification error
toRemove.add(innerComponent);
}
for (final Component<?> innerComponent : toRemove) {
// Remove it from the list
getInnerComponentList().get().remove(innerComponent);
// Then remove it from map
getInnerComponentMap().get().remove(innerComponent.getKey());
}
}
// } catch (final JRebirthThreadException jte) {
// LOGGER.error(COMPONENT_RELEASE_ERROR, jte);
// }
}
// public boolean isReady() {
// return ready;
// }
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public final <IC extends Component<?>> void addInnerComponent(final InnerComponent<IC> innerComponent) {
if (!getInnerComponentMap().isPresent()) {
// Initialize default storage objects only on demand (to save some bits)
this.innerComponentMap = new IdentityHashMap<>(10);
this.innerComponentList = new ArrayList<Component<?>>();
}
// If the inner model hasn't been loaded before, build it from UIFacade
if (!this.innerComponentMap.containsKey(innerComponent)) {
final Class<IC> innerComponentClass = innerComponent.getKey().getClassField();
IC childComponent = null;
// Use the right facade according to the inner component type
if (Command.class.isAssignableFrom(innerComponentClass)) {
// Initialize the Inner Command
childComponent = (IC) getLocalFacade().getGlobalFacade().getCommandFacade().retrieve((UniqueKey<Command>) innerComponent.getKey());
} else if (Service.class.isAssignableFrom(innerComponentClass)) {
// Initialize the Inner Service
childComponent = (IC) getLocalFacade().getGlobalFacade().getServiceFacade().retrieve((UniqueKey<Service>) innerComponent.getKey());
} else if (Model.class.isAssignableFrom(innerComponentClass)) {
// Initialize the Inner Model
childComponent = (IC) getLocalFacade().getGlobalFacade().getUiFacade().retrieve((UniqueKey<Model>) innerComponent.getKey());
} else if (Behavior.class.isAssignableFrom(innerComponentClass)) {
// Cannot initialize Inner Behavior, they cannot be nested
throw new CoreRuntimeException("Behaviors can not be used as Inner EnhancedComponent"); // FIXME add MessageItem
}
// Inner EnhancedComponent creation has failed throw an exception
if (childComponent == null) {
throw new CoreRuntimeException("Impossible to create Inner EnhancedComponent"); // FIXME add MessageItem
}
// Store the component into the multitonKey map
this.innerComponentMap.put(innerComponent, childComponent);
this.innerComponentList.add(childComponent);
// Link the current root model
childComponent.setRootComponent(this);
}
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public final <IC extends Component<?>> IC getInnerComponent(final InnerComponent<IC> innerModel) {
if (!getInnerComponentMap().isPresent() || !getInnerComponentMap().get().containsKey(innerModel)) {
// This InnerModel should be initialized into the initInnerModel method instead
// but in some cases a late initialization can help
addInnerComponent(innerModel);
}
return (IC) getInnerComponentMap().get().get(innerModel);
}
/**
* {@inheritDoc}
*/
@Override
public Component<?> getRootComponent() {
return this.rootComponent;
}
/**
* {@inheritDoc}
*/
@Override
public void setRootComponent(final Component<?> rootComponent) {
this.rootComponent = rootComponent;
}
/**
* Initialize the included models.
*
* This method is a hook to manage generic code before initializing inner models.
*
* You must implement the {@link #initInnerComponents()} method to setup your inner models.
*/
protected final void initInternalInnerComponents() {
// Do generic stuff
// Do custom stuff
initInnerComponents();
}
/**
* Initialize method that should wrap all creation of {@link InnerComponent}.
*
* It shall contain only {@link EnhancedComponent}.addInnerComponent calls.
*/
protected abstract void initInnerComponents();
/**
* Return the InnerComponent map wrapped into an Optional because it's initialized only on demand.
*
* @return the innerComponentMap.
*/
protected Optional<Map<InnerComponent<?>, Component<?>>> getInnerComponentMap() {
return Optional.ofNullable(this.innerComponentMap);
}
/**
* Return the InnerComponent list wrapped into an Optional because it's initialized only on demand.
*
* @return the innerComponentList.
*/
protected Optional<List<Component<?>>> getInnerComponentList() {
return Optional.ofNullable(this.innerComponentList);
}
}