diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java index 0f9c324aee1f6c5d5222164b3f3fe0f793b26545..40bf290e4ceb96194974aca7bd0ddbf58c2fe6a9 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java @@ -35,17 +35,17 @@ public class CheckStateTransition extends EntityStateJob { @Override protected void run() { try { - State nextState = getState(); - if (nextState != null) { - checkStateValid(nextState); + State newState = getState(); + if (newState != null) { + checkStateValid(newState); } if (getEntity() instanceof UpdateEntity) { - State fromState = getState(((UpdateEntity) getEntity()).getOriginal()); - checkStateTransition(fromState, nextState); + State oldState = getState(((UpdateEntity) getEntity()).getOriginal()); + checkStateTransition(oldState, newState); } else if (getEntity() instanceof DeleteEntity) { - if (nextState != null) checkFinalState(nextState); + if (newState != null) checkFinalState(newState); } else { - if (nextState != null) checkInitialState(nextState); + if (newState != null) checkInitialState(newState); } } catch (Message m) { getEntity().addError(m); @@ -68,31 +68,31 @@ public class CheckStateTransition extends EntityStateJob { /** * Check if state is valid and transition is allowed. * - * @param fromState - * @param toState + * @param oldState + * @param newState * @throws Message if not */ - private void checkStateTransition(State fromState, State toState) throws Message { - if (fromState == null && toState == null) { + private void checkStateTransition(State oldState, State newState) throws Message { + if (oldState == null && newState == null) { return; - } else if (fromState == null && toState != null) { - checkInitialState(toState); + } else if (oldState == null && newState != null) { + checkInitialState(newState); return; - } else if (toState == null && fromState != null) { - checkFinalState(fromState); + } else if (newState == null && oldState != null) { + checkFinalState(oldState); return; } - StateModel stateModel = findMatchingStateModel(fromState, toState); + StateModel stateModel = findMatchingStateModel(oldState, newState); if (stateModel == null) { // change from one stateModel to another - checkInitialState(toState); - checkFinalState(fromState); + checkInitialState(newState); + checkFinalState(oldState); return; } for (Transition t : stateModel.getTransitions()) { - if (t.fromStatesInclude(fromState) && t.toStatesInclude(toState)) { + if (t.isFromState(oldState) && t.isToState(newState)) { // TODO permissions return; } @@ -101,14 +101,14 @@ public class CheckStateTransition extends EntityStateJob { } /** - * @param fromState - * @param toState + * @param oldState + * @param newState * @return the state model which contains both of the states. * @throws Message if the state model of one of the states cannot be constructed. */ - private StateModel findMatchingStateModel(State fromState, State toState) throws Message { - if (fromState.getStateModel().equals(toState.getStateModel())) { - return fromState.getStateModel(); + private StateModel findMatchingStateModel(State oldState, State newState) throws Message { + if (oldState.getStateModel().equals(newState.getStateModel())) { + return oldState.getStateModel(); } return null; } @@ -116,11 +116,11 @@ public class CheckStateTransition extends EntityStateJob { /** * Check if the old state is final or if the {@link FORCE_FINAL_STATE} flag is true. * - * @param fromState + * @param oldState * @throws Message if the state is not final. */ - private void checkFinalState(State fromState) throws Message { - if (!fromState.isFinal()) { + private void checkFinalState(State oldState) throws Message { + if (!oldState.isFinal()) { if ("true".equalsIgnoreCase(getTransaction().getContainer().getFlags().get(FORCE_FINAL_STATE)) || "true".equalsIgnoreCase(getEntity().getFlag(FORCE_FINAL_STATE))) { // TODO permissions @@ -134,11 +134,11 @@ public class CheckStateTransition extends EntityStateJob { /** * Check if the new state is an initial state. * - * @param toState + * @param newState * @throws Message if not */ - private void checkInitialState(State toState) throws Message { - if (!toState.isInitial()) { + private void checkInitialState(State newState) throws Message { + if (!newState.isInitial()) { throw INITIAL_STATE_NOT_ALLOWED; } // TODO permissions diff --git a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java index 0602c2025b58bba0bab4652b7654fe8cb418bba6..30a4de8e04d69ad3c4b1ea1c1ebc9a082d8f4920 100644 --- a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java +++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java @@ -22,6 +22,7 @@ import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.entity.xml.ToElementable; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.query.Query; +import org.caosdb.server.utils.EntityStatus; import org.jdom2.Element; /** @@ -111,11 +112,19 @@ public abstract class EntityStateJob extends EntityJob { return null; } - public boolean fromStatesInclude(State previousState) { + /** + * @param previousState + * @return true iff the previous state is a fromState of this transition. + */ + public boolean isFromState(State previousState) { return this.fromState.equals(previousState); } - public boolean toStatesInclude(State nextState) { + /** + * @param nextState + * @return true iff the next state is a toState of this transition. + */ + public boolean isToState(State nextState) { return this.toState.equals(nextState); } @@ -168,6 +177,9 @@ public abstract class EntityStateJob extends EntityJob { * <p>Furthermore, States are the start or end point of Transitions which belong to the same * StateModel. Each State can be part of several transitions at the same time. * + * <p>Note: The purpose of this should not be confused with {@link EntityStatus} which is purely + * for internal use. + * * @author Timm Fitschen (t.fitschen@indiscale.com) */ public class State implements ToElementable { @@ -205,6 +217,7 @@ public abstract class EntityStateJob extends EntityJob { return 21364234 + this.getStateName().hashCode() + this.getStateModelName().hashCode(); } + /** Serialize this State into XML. */ @Override public void addToElement(Element ret) { Element e = new Element(STATE_XML_TAG); @@ -232,10 +245,18 @@ public abstract class EntityStateJob extends EntityJob { return this.stateModel; } + /** + * @return true iff this state is an initial state of its StateModel. + * @throws Message + */ public boolean isInitial() throws Message { return Objects.equals(this, getStateModel().initialState); } + /** + * @return true iff this state is a final state of its StateModel. + * @throws Message + */ public boolean isFinal() throws Message { return Objects.equals(this, getStateModel().finalState); } @@ -337,6 +358,13 @@ public abstract class EntityStateJob extends EntityJob { return null; } + /** + * Read out the "Transition" property and create Transition instances. + * + * @param p the transition property + * @return a set of transitions + * @throws Message if the transitions could ne be created. + */ private Set<Transition> createTransitions(Property p) throws Message { Set<Transition> result = new HashSet<>(); try { @@ -358,6 +386,17 @@ public abstract class EntityStateJob extends EntityJob { return result; } + /** + * Collect all possible states from the set of transitions. + * + * <p>This function does not perform any consistency checks. It only add all toStates and + * fromStates of the transitions to the result. + * + * @param transitions + * @param stateModel + * @return set of states. + * @throws Message + */ private Set<State> getStates(Set<Transition> transitions, StateModel stateModel) throws Message { Iterator<Transition> it = transitions.iterator(); @@ -434,7 +473,6 @@ public abstract class EntityStateJob extends EntityJob { } protected EntityInterface getStateRecordType() throws Message { - // TODO this should be cached return retrieveValidSparseEntityByName(STATE_RECORD_TYPE_NAME); } @@ -517,6 +555,15 @@ public abstract class EntityStateJob extends EntityJob { return new State(stateName, stateModel); } + /** + * Create a State instance from the value of the state property. + * + * <p>This method also retrieves the state entity from the back-end. + * + * @param p the entity's state property + * @return The state of the entity + * @throws Message + */ protected State createState(Property p) throws Message { try { p.parseValue(); @@ -525,6 +572,8 @@ public abstract class EntityStateJob extends EntityJob { EntityInterface stateModelEntity = findStateModel(stateEntity); return new State(stateEntity, stateModelEntity); + } catch (Message e) { + throw e; } catch (Exception e) { throw COULD_NOT_CONSTRUCT_STATE_MESSAGE; } diff --git a/src/main/java/org/caosdb/server/jobs/core/InitEntityState.java b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java similarity index 51% rename from src/main/java/org/caosdb/server/jobs/core/InitEntityState.java rename to src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java index 4b1d74d9827b88bb286fac0d4a1f9b494eb803ad..e7dc54af6d6f951725cc66e6839a476ba4cdb060 100644 --- a/src/main/java/org/caosdb/server/jobs/core/InitEntityState.java +++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java @@ -17,43 +17,75 @@ import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; import org.caosdb.server.utils.Observer; +/** + * Initialize the other entity jobs by converting the client message with type "State" into {@link + * State} instances. + * + * <p>This job also needs to initialize the other jobs even if the current entity version does not + * have a state anymore but the previous version had, because it has to be checked if the stateModel + * allows to leave in this state. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ @JobAnnotation( loadAlways = true, time = JobExecutionTime.INIT, transaction = WriteTransaction.class) -public class InitEntityState extends EntityStateJob implements Observer { +public class InitEntityStateJobs extends EntityStateJob implements Observer { @Override protected void run() { + State newState = handleNewState(); + handleOldState(newState); + } + + /** + * Converts the state property of the original entity into a state message (only needed for + * updates). + * + * <p>Also, this method adds an observer to the entity state which handles a corner case where the + * entity changes the state, but no other property changes. In this case Update.deriveUpdate + * cannot detect any changes and will mark this entity as "to-be-skipped". The observer waits for + * that to happen and changes the {@EntityStatus} back to normal. + * + * @param newState + */ + private void handleOldState(State newState) { try { - State newState = null; - { - List<State> states = initStateMessage(getEntity()); - if (states.size() > 1) { - throw new Message( - MessageType.Error, "Currently, each entity can only have one state at a time."); - } else if (states.size() == 1) { - newState = states.get(0); + if (getEntity() instanceof UpdateEntity) { + List<State> states = initStateMessage(((UpdateEntity) getEntity()).getOriginal()); + State oldState = null; + if (states.size() == 1) { + oldState = states.get(0); } - } - try { - if (getEntity() instanceof UpdateEntity) { - List<State> states = initStateMessage(((UpdateEntity) getEntity()).getOriginal()); - State oldState = null; - if (states.size() == 1) { - oldState = states.get(0); - } - if (!Objects.equals(newState, oldState)) { - getEntity().acceptObserver(this); - } + if (!Objects.equals(newState, oldState)) { + getEntity().acceptObserver(this); } - } catch (Message m) { - getEntity().addWarning(STATE_ERROR_IN_ORIGINAL_ENTITY(m)); + } + } catch (Message m) { + getEntity().addWarning(STATE_ERROR_IN_ORIGINAL_ENTITY(m)); + } + } + + /** + * Converts the state property of this entity into a state message. + * + * @return The new state. + */ + private State handleNewState() { + State newState = null; + try { + List<State> states = initStateMessage(getEntity()); + if (states.size() > 1) { + throw new Message( + MessageType.Error, "Currently, each entity can only have one state at a time."); + } else if (states.size() == 1) { + newState = states.get(0); } } catch (Message m) { getEntity().addError(m); - return; } + return newState; } private static final Message STATE_ERROR_IN_ORIGINAL_ENTITY(Message m) { @@ -61,6 +93,13 @@ public class InitEntityState extends EntityStateJob implements Observer { MessageType.Warning, "State error in previous entity version\n" + m.getDescription()); } + /** + * Returns a list of states as the are represented as properties or client messages in the entity. + * + * @param entity + * @return list of state instances for the entity. + * @throws Message + */ private List<State> initStateMessage(EntityInterface entity) throws Message { List<ClientMessage> stateClientMessages = getStateClientMessages(entity, true); List<State> result = new ArrayList<>();