/*******************************************************************************
* Copyright 2006 - 2014 Vienna University of Technology,
* Department of Software Technology and Interactive Systems, IFS
*
* 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 eu.scape_project.planning.manager;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import javax.ejb.Stateful;
import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.hibernate.Hibernate;
import org.slf4j.Logger;
import eu.scape_project.planning.exception.PlanningException;
import eu.scape_project.planning.model.AlternativesDefinition;
import eu.scape_project.planning.model.DigitalObject;
import eu.scape_project.planning.model.Plan;
import eu.scape_project.planning.model.PlanProperties;
import eu.scape_project.planning.model.PlanState;
import eu.scape_project.planning.model.User;
import eu.scape_project.planning.model.Values;
import eu.scape_project.planning.model.measurement.Measure;
import eu.scape_project.planning.model.transform.OrdinalTransformer;
import eu.scape_project.planning.model.transform.Transformer;
import eu.scape_project.planning.model.tree.Leaf;
import eu.scape_project.planning.model.tree.Node;
import eu.scape_project.planning.model.tree.ObjectiveTree;
import eu.scape_project.planning.model.tree.TreeNode;
import eu.scape_project.planning.utils.FacesMessages;
/**
* Stateful session bean for managing plans.
*/
@Stateful
@SessionScoped
@Named("planManager")
public class PlanManager implements Serializable {
private static final long serialVersionUID = -1L;
/**
* Selection of projects to query.
*/
public enum WhichProjects {
ALLPROJECTS,
PUBLICPROJECTS,
MYPROJECTS;
}
@Inject
private Logger log;
@Inject
private EntityManager em;
@Inject
private ByteStreamManager bytestreamManager;
private WhichProjects lastLoadMode = WhichProjects.MYPROJECTS;
@Inject
private User user;
@Inject
private FacesMessages facesMessages;
/**
* Plan properties of all loaded plans in this session.
*/
private HashSet<Integer> sessionPlans;
/**
* Query to get PlanProperties.
*/
public class PlanQuery {
private CriteriaBuilder builder;
private CriteriaQuery<PlanProperties> cq;
private Root<Plan> fromPlan;
private Path<PlanProperties> fromPP;
private List<Predicate> visibilityPredicates;
private List<Predicate> stateFilterPredicates;
private List<Predicate> nameFilterPredicates;
private Predicate mappedFilterPredicate;
private Predicate playgroundFilterPredicate;
/**
* Initializes the query.
*/
private void init() {
builder = em.getCriteriaBuilder();
cq = builder.createQuery(PlanProperties.class);
// From
fromPlan = cq.from(Plan.class);
fromPP = fromPlan.<PlanProperties> get("planProperties");
// Select
cq.select(fromPlan.<PlanProperties> get("planProperties"));
visibilityPredicates = new ArrayList<Predicate>(2);
stateFilterPredicates = new ArrayList<Predicate>();
nameFilterPredicates = new ArrayList<Predicate>();
}
/**
* Adds a visibility criteria to the query.
*
* @param whichProjects
* criteria to add
* @return this plan query
*/
public PlanQuery addVisibility(WhichProjects whichProjects) {
// Select usernames of the users group
if (whichProjects == WhichProjects.MYPROJECTS) {
Subquery<User> subquery = cq.subquery(User.class);
Root<User> fromUser = subquery.from(User.class);
subquery.select(fromUser.<User> get("username"));
subquery.where(builder.equal(fromUser.get("userGroup"), user.getUserGroup()));
visibilityPredicates.add(fromPP.get("owner").in(subquery));
} else if (whichProjects == WhichProjects.PUBLICPROJECTS) {
visibilityPredicates.add(builder.or(builder.isFalse(fromPP.<Boolean> get("privateProject")),
builder.isTrue(fromPP.<Boolean> get("reportPublic"))));
} else if (whichProjects == WhichProjects.ALLPROJECTS) {
if (user.isAdmin()) {
// Always true
visibilityPredicates.add(builder.conjunction());
}
}
return this;
}
/**
* Adds plan states as criteria to the query.
*
* @param planStates
* the plan states to add
* @return this query
*/
public PlanQuery filterState(PlanState... planStates) {
if (planStates.length == 0) {
return this;
}
stateFilterPredicates.add(fromPP.<PlanState> get("state").in((Object[]) planStates));
return this;
}
/**
* Adds a minimum plan state to the query. Plans will have at least the
* state provided.
*
* @param planState
* the plan state
* @return this query
*/
public PlanQuery filterMinState(PlanState planState) {
PlanState[] planStates = new PlanState[PlanState.values().length - planState.ordinal()];
int i = 0;
for (PlanState p : PlanState.values()) {
if (p.compareTo(planState) >= 0) {
planStates[i] = p;
i++;
}
}
return filterState(planStates);
}
/**
* Adds a filter for the plan name. The query matches plans with a name
* like the filter string.
*
* If no filter was added to the query, plans with any name matches.
*
* @param filter
* the filter string
* @return this query
*/
public PlanQuery filterNameLike(String filter) {
nameFilterPredicates.add(builder.like(fromPP.<String> get("name"), filter));
return this;
}
/**
* Adds a filter for the plan name. The query matches plans with a name
* unlike the filter string.
*
* If no filter was added to the query, plans with any name matches.
*
* @param filter
* the filter string
* @return this query
*/
public PlanQuery filterNameUnlike(String filter) {
nameFilterPredicates.add(builder.notLike(fromPP.<String> get("name"), filter));
return this;
}
/**
* Adds a filter to query only for plans with mapped measures.
*
* @return this query
*/
public PlanQuery filterMapped() {
Subquery<Integer> subquery = cq.subquery(Integer.class);
Root<Leaf> fromTreeNode = subquery.from(Leaf.class);
subquery.select(fromTreeNode.<Integer> get("id"));
subquery.where(builder.and(
fromTreeNode.<Measure> get("measure").isNotNull(),
builder.equal(builder.function("rootNode", Integer.class, fromTreeNode.<Integer> get("id")), fromPlan
.<ObjectiveTree> get("tree").<TreeNode> get("root").<Integer> get("id"))));
mappedFilterPredicate = builder.exists(subquery);
return this;
}
/**
* Adds a filter excluding all plans marked as playground.
*
* @return this query
*/
public PlanQuery filterPlayground() {
playgroundFilterPredicate = builder.isFalse(fromPP.<Boolean> get("playground"));
return this;
}
/**
* Finishes the query.
*/
private void finishQuery() {
List<Predicate> predicates = new ArrayList<Predicate>(5);
// Where
predicates.add(builder.or(visibilityPredicates.toArray(new Predicate[visibilityPredicates.size()])));
if (stateFilterPredicates.size() > 0) {
predicates.add(builder.or(stateFilterPredicates.toArray(new Predicate[stateFilterPredicates.size()])));
}
if (nameFilterPredicates.size() > 0) {
predicates.add(builder.or(nameFilterPredicates.toArray(new Predicate[nameFilterPredicates.size()])));
}
if (mappedFilterPredicate != null) {
predicates.add(mappedFilterPredicate);
}
if (playgroundFilterPredicate != null) {
predicates.add(playgroundFilterPredicate);
}
cq.where(builder.and(predicates.toArray(new Predicate[predicates.size()])));
// Order by
cq.orderBy(builder.asc(fromPP.get("id")));
}
}
/**
* Constructs a new plan manager.
*/
public PlanManager() {
sessionPlans = new HashSet<Integer>();
}
/**
* Unlocks all plans opened in this session.
*/
public void unlockSessionPlans() {
HashSet<Integer> lockedPlans = new HashSet<Integer>(sessionPlans);
for (Integer planPropertiesId : lockedPlans) {
unlockPlan(planPropertiesId);
}
}
/**
* Creates a new plan query.
*
* @return the plan query
*/
public PlanQuery createQuery() {
PlanQuery ps = new PlanQuery();
ps.init();
return ps;
}
/**
* Returns all plans that fit the plan query.
*
* @param planQuery
* the plan query
* @return the plans
*/
public List<PlanProperties> list(PlanManager.PlanQuery planQuery) {
planQuery.finishQuery();
TypedQuery<PlanProperties> query = em.createQuery(planQuery.cq);
List<PlanProperties> planProperties = query.getResultList();
List<String> usernames = em
.createQuery("SELECT u.username from User u WHERE u.userGroup = :userGroup", String.class)
.setParameter("userGroup", user.getUserGroup()).getResultList();
for (PlanProperties pp : planProperties) {
// A plan may be edited:
// user currently logged in is administrator
// or user currently logged in is the owner
// or user currently logged in is in the group of the owner
boolean mayEdit = pp.isClosed()
&& (user.isAdmin() || user.getUsername().equals(pp.getOwner()) || usernames.contains(pp.getOwner()));
pp.setMayEdit(mayEdit);
pp.setAllowUnlock(pp.getOpenedByUser().equals(user.getUsername()) || user.isAdmin());
}
return planProperties;
}
/**
* Reloads a plan. Checks if the provided plan is opened by the current
* user.
*
* @param plan
* the plan to reload
* @return the reloaded plan
* @throws PlanningException
* if the plan could not be reloaded
*/
public Plan reloadPlan(Plan plan) throws PlanningException {
TypedQuery<Long> q = em
.createQuery(
"select count(pp.id) from PlanProperties pp where (pp.openHandle = 1) and (pp.openedByUser = :user) and (pp.id = :propid)",
Long.class);
q.setParameter("user", user.getUsername());
q.setParameter("propid", plan.getPlanProperties().getId());
Long planCount = q.getSingleResult();
if (planCount != 1) {
throw new PlanningException("This plan has not been loaded before, reload is not possible.");
}
Plan reloadedPlan = em.find(Plan.class, plan.getId());
this.initializePlan(reloadedPlan);
log.info("Plan " + reloadedPlan.getPlanProperties().getName() + " reloaded!");
return reloadedPlan;
}
/**
* Loads the plan with the given plan-Id from the database. - without
* locking the plan!
*
* @param planId
* the plan ID
* @return the loaded plan
*/
public Plan loadPlan(int planId) {
Plan plan = em.find(Plan.class, planId);
this.initializePlan(plan);
return plan;
}
/**
* Loads the given plan from the database and locks it if requested.
*
* @param propertyId
* the plan's PROPERTIES id!
* @param readOnly
* states if the plan should be opened in read only mode
* @return the loaded plan
* @throws PlanningException
* if the plan could not be loaded
*/
public Plan load(int propertyId, boolean readOnly) throws PlanningException {
if (!readOnly) {
// try to lock the plan
Query q = em
.createQuery("update PlanProperties pp set pp.openHandle = 1, pp.openedByUser = :user where (pp.openHandle is null or pp.openHandle = 0) and pp.id = :propid");
q.setParameter("user", user.getUsername());
q.setParameter("propid", propertyId);
int num = q.executeUpdate();
if (num < 1) {
throw new PlanningException("The plan has been loaded by another user. Please choose another plan.");
}
// and add it to the list of loaded plans, so we can unlock it in
// any case
sessionPlans.add(propertyId);
}
// then load the plan
Object result = em.createQuery("select p.id from Plan p where p.planProperties.id = " + propertyId)
.getSingleResult();
if (result != null) {
Plan plan = loadPlan((Integer) result);
plan.setReadOnly(readOnly);
log.info("Plan {} : {} loaded.", propertyId, plan.getPlanProperties().getName());
return plan;
} else {
throw new PlanningException("An unexpected error has occured while loading the plan.");
}
}
/**
* Hibernate initializes project and its parts.
*
* @param p
* the plan to initialize
*/
private void initializePlan(Plan p) {
Hibernate.initialize(p);
Hibernate.initialize(p.getAlternativesDefinition());
Hibernate.initialize(p.getSampleRecordsDefinition());
Hibernate.initialize(p.getTree());
initializeNodeRec(p.getTree().getRoot());
log.debug("plan initialised");
}
/**
* Traverses down the nodes in the tree and calls
* <code>Hibernate.initialize</code> for each leaf. This is necessary to
* provide the application with a convenient way of working with lazily
* initialized collections or proxies.
*
* @param node
* node from where initialization shall start
*/
private void initializeNodeRec(TreeNode node) {
Hibernate.initialize(node);
if (node.isLeaf()) {
Leaf leaf = (Leaf) node;
Transformer t = leaf.getTransformer();
Hibernate.initialize(t);
if (t instanceof OrdinalTransformer) {
OrdinalTransformer nt = (OrdinalTransformer) t;
Hibernate.initialize(nt.getMapping());
}
// log.debug("hibernate initialising Transformer: " +
// leaf.getTransformer());
for (Values value : leaf.getValueMap().values()) {
Hibernate.initialize(value);
}
} else if (node instanceof Node) {
Node recnode = (Node) node;
Hibernate.initialize(node.getChildren());
for (TreeNode newNode : recnode.getChildren()) {
initializeNodeRec(newNode);
}
}
}
/**
* Unlocks all plans in the database.
*/
public void unlockAll() {
this.unlockQuery(-1);
}
/**
* Unlocks a plan with the provided plan properties ID.
*
* @param planPropertiesId
* the plan's PROPERTIES id
*/
public void unlockPlan(int planPropertiesId) {
// remove from list of locked plans
sessionPlans.remove(planPropertiesId);
unlockQuery(planPropertiesId);
}
/**
* Unlocks plans in the database (dependent on parameter). If the pid is -1,
* all plans are unlocked, otherwise the plan with the provided pid is
* unlocked.
*
* @param pid
* The plan ID to unlock or -1 to unlock all plans
*/
private void unlockQuery(long pid) {
String where = "";
if (pid > -1) {
where = "where pp.id = " + pid;
}
Query q = em.createQuery("update PlanProperties pp set pp.openHandle = 0, pp.openedByUser = '' " + where);
try {
if (q.executeUpdate() < 1) {
log.error("Unlocking plan of plans with with id [{}] failed.", pid);
} else {
log.info("Unlocked plans with id [{}].", pid);
}
} catch (Throwable e) {
log.error("Unlocking plans with id [{}] failed:", pid, e);
}
}
/**
* Updates the state of the provided plan and saves the provided entity.
*
* @param plan
* the plan
* @param currentState
* the state of the plan
* @param entity
* the entity to save
*/
public void save(Plan plan, PlanState currentState, Object entity) {
log.debug("Persisting plan " + entity.getClass().getName());
plan.getPlanProperties().setState(currentState);
if (plan.getPlanProperties().getReportUpload().isDataExistent()) {
plan.getPlanProperties().setReportUpload(new DigitalObject());
String msg = "Please consider that because data underlying the preservation plan has been changed, the uploaded report was automatically removed. ";
msg += "If you would like to make the updated report available, please generate it again and upload it in 'Plan Settings'.";
facesMessages.addInfo(msg);
}
PlanProperties planProperties = em.merge(plan.getPlanProperties());
em.persist(planProperties);
plan.setPlanProperties(planProperties);
em.persist(em.merge(entity));
}
// --------------- save operations for steps ---------------
/**
* Saves changes to the plan settings.
*
* @param planProperties
* the plan properties
* @param alternativesDefinition
* alternatives to save
*/
public void saveForPlanSettings(PlanProperties planProperties, AlternativesDefinition alternativesDefinition) {
em.persist(em.merge(planProperties));
em.persist(em.merge(alternativesDefinition));
}
// --------------- delete a plan from database ---------------
/**
* Method responsible for deleting a plan from database.
*
* @param plan
* the plan to delete.
* @throws PlanningException
* if the plan could not be deleted
*/
public void deletePlan(Plan plan) throws PlanningException {
if (plan.isReadOnly()) {
throw new PlanningException("Plans opened in read only mode cannot be deleted!");
}
log.info("Deleting plan {} with pid {}", plan.getPlanProperties().getName(), plan.getPlanProperties().getId());
List<DigitalObject> digitalObjects = plan.getDigitalObjects();
try {
em.remove(em.merge(plan));
em.flush();
for (DigitalObject obj : digitalObjects) {
bytestreamManager.delete(obj.getPid());
}
} catch (StorageException e) {
throw e;
} catch (Exception e) {
throw new PlanningException("Failed to delete plan: " + plan.getPlanProperties().getName() + " with id: "
+ plan.getPlanProperties().getId(), e);
}
}
// ********** getter/setter **********
public WhichProjects getLastLoadMode() {
return lastLoadMode;
}
public void setLastLoadMode(WhichProjects lastLoadMode) {
this.lastLoadMode = lastLoadMode;
}
}