org.yawlfoundation.yawl.scheduling.Scheduler.java Source code

Java tutorial

Introduction

Here is the source code for org.yawlfoundation.yawl.scheduling.Scheduler.java

Source

/*
 * Copyright (c) 2004-2012 The YAWL Foundation. All rights reserved.
 * The YAWL Foundation is a collaboration of individuals and
 * organisations who are committed to improving workflow technology.
 *
 * This file is part of YAWL. YAWL is free software: you can
 * redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation.
 *
 * YAWL is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with YAWL. If not, see <http://www.gnu.org/licenses/>.
 */

package org.yawlfoundation.yawl.scheduling;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.Document;
import org.jdom2.Element;
import org.yawlfoundation.yawl.scheduling.persistence.DataMapper;
import org.yawlfoundation.yawl.scheduling.resource.ResourceServiceInterface;
import org.yawlfoundation.yawl.scheduling.util.PropertyReader;
import org.yawlfoundation.yawl.scheduling.util.Utils;
import org.yawlfoundation.yawl.scheduling.util.XMLUtils;

import javax.xml.datatype.Duration;
import java.util.*;

/**
 * (re)schedules RUPs automatically
 * 
 * @author tbe
 * @version $Id$
 */
public class Scheduler implements Constants {
    private static Logger logger = LogManager.getLogger(Scheduler.class);

    private DataMapper dataMapper;
    private ResourceServiceInterface rs;
    private final boolean debug = true;

    public Scheduler() {
        dataMapper = new DataMapper();
        rs = ResourceServiceInterface.getInstance();
    }

    /**
     * set times of activities if FROM and DURATION are given Rescheduling
     * kollidierender RUPs, welche definiert sind als: alle RUPs welche die
     * selben Ressourcen zur gleichen Zeit definieren wie der vorhergehend RUP.
     * als selbe Ressourcen werden nur nicht austauschbare Ressourcen betrachtet,
     * das sind Ressourcen mit Reservierung by Id (TODO@tbe: Reservierungen by
     * Role oder Category knnten auch kollidieren, hier aber erstmal weglassen)
     * TODO@tbe: Rescheduling von RUPs die Leerlaufzeiten haben, z.B. durch
     * frheres ende einer Aktivitt, erstmal nicht betrachten
     * 
     * @param rup
     * @param activity
     * @param withValidation
     * @param rescheduleCollidingRUPs
     * 
     */
    public boolean setTimes(Document rup, Element activity, boolean withValidation, boolean rescheduleCollidingRUPs,
            Duration defaultDuration) {
        String caseId = XMLUtils.getCaseId(rup);
        String activityName = activity.getChildText(XML_ACTIVITYNAME);
        boolean hasRescheduledRUPs = false;

        try {
            // 1) reschedule rup
            setTimes(rup, activity, withValidation, new ArrayList<String>(), defaultDuration);

            // rescheduling colliding rups
            if (rescheduleCollidingRUPs) {
                List<Document> allCollidingRUPs = new ArrayList<Document>();
                List<String> allCollidingRUPCaseIds = new ArrayList<String>();
                allCollidingRUPCaseIds.add(caseId);

                // 2) collect all potential colliding rups and remove their
                // reservations
                List<Document> collidingRUPs = getCollidingRups(rup, allCollidingRUPCaseIds);
                while (!collidingRUPs.isEmpty()) {
                    List<Document> newCollidingRUPs = new ArrayList<Document>();
                    for (Document collidingRUP : collidingRUPs) {
                        String collCaseId = null;
                        try {
                            collCaseId = XMLUtils.getCaseId(collidingRUP);
                            if (allCollidingRUPCaseIds.contains(collCaseId))
                                continue;

                            // remove all reservations in RS by saving rup without
                            // reservations
                            Map<String, List<Element>> collRes = rs.removeReservations(collidingRUP, null);
                            try {
                                logger.debug("delete reservations from collidingRUP: " + collCaseId);
                                collidingRUP = rs.saveReservations(collidingRUP, false, false);
                            } finally {
                                rs.addReservations(collidingRUP, collRes);
                            }

                            allCollidingRUPs.add(collidingRUP);
                            allCollidingRUPCaseIds.add(collCaseId);

                            newCollidingRUPs.addAll(getCollidingRups(collidingRUP, allCollidingRUPCaseIds));
                        } catch (Exception e) {
                            logger.error("cannot collect caseId: " + (collCaseId == null ? "null" : collCaseId), e);
                            XMLUtils.addErrorValue(rup.getRootElement(), withValidation, "msgRescheduleError",
                                    activityName, e.getMessage());
                        }
                    }
                    collidingRUPs = newCollidingRUPs;
                }

                if (!allCollidingRUPs.isEmpty()) {
                    // 3) save rup with new times, should not conflicting with other
                    // rups
                    // logger.debug("----------------wait 30s and save: "+caseId);
                    // Thread.sleep(30000); // FIXME@tbe: raus
                    Set<String> errors = SchedulingService.getInstance().optimizeAndSaveRup(rup, "reschedulingRUP",
                            null, false);
                    logger.debug("----------------save rescheduled rup caseId: " + caseId + ", errors: "
                            + Utils.toString(errors));
                }

                // 4) sort colliding rups
                Collections.sort(allCollidingRUPs, new Comparator<Document>() {
                    public int compare(Document rup1, Document rup2) {
                        Date earlFrom1 = XMLUtils.getEarliestBeginDate(rup1);
                        Date earlFrom2 = XMLUtils.getEarliestBeginDate(rup2);
                        long timeGap = earlFrom1.getTime() - earlFrom2.getTime();
                        if (timeGap > 0)
                            return 1;
                        else if (timeGap < 0)
                            return -1;
                        else
                            return 0;
                    }
                });

                // 5) find new time slot for each colliding rup and save it
                for (Document collidingRUP : allCollidingRUPs) {
                    String collCaseId = null;
                    try {
                        collCaseId = XMLUtils.getCaseId(collidingRUP);
                        if (collCaseId.equals(caseId))
                            continue;

                        // TODO@tbe: so ist nur das suchen eines spteren timeslots
                        // mglich, es sollte auch
                        // ein frherer mglich sein, falls RUP nach vorne verschoben
                        // wurde, dazu evntl.
                        // from auf 00:00 Uhr setzen, dann findTimeSlot aufrufen
                        hasRescheduledRUPs = findTimeSlot(collidingRUP, true) || hasRescheduledRUPs;

                        SchedulingService.getInstance().optimizeAndSaveRup(collidingRUP,
                                "reschedulingCollidingRUPs", null, false);
                        logger.debug("save rescheduled colliding caseId: " + collCaseId + ", errors: "
                                + Utils.toString(XMLUtils.getErrors(collidingRUP.getRootElement())));
                        logger.info("caseId: " + collCaseId + " successfully rescheduled");
                    } catch (Exception e) {
                        logger.error("cannot reschedule caseId: " + (collCaseId == null ? "null" : collCaseId), e);
                        XMLUtils.addErrorValue(rup.getRootElement(), withValidation, "msgRescheduleError",
                                activityName, e.getMessage());
                    }
                }
            }
        } catch (Exception e) {
            logger.error("error during rescheduling caseId: " + (caseId == null ? "null" : caseId), e);
            XMLUtils.addErrorValue(rup.getRootElement(), withValidation, "msgRescheduleError", activityName,
                    e.getMessage());
        }

        return hasRescheduledRUPs;
    }

    /**
     * set TO time of an activity depending on FROM and DURATION set FROM time of
     * previous and following activities and recurses to set their TO times also
     * 
     * TODO@tbe: erstmal ohne Beachtung von Max und ohne Ausnutzung des
     * Gummibandes zwischen Min und Max, d.h. es wird Min als fester Wert
     * angenommen, ausserdem wird nur von einer Relation pro Aktivitt
     * ausgegangen
     * 
     * @param activity
     */
    private void setTimes(Document rup, Element activity, boolean withValidation,
            List<String> activityNamesProcessed, Duration defaultDuration) {
        String activityName = activity.getChildText(XML_ACTIVITYNAME);
        Element durationElem = activity.getChild(XML_DURATION);
        Duration duration = XMLUtils.getDurationValue(durationElem, withValidation);
        Element from = activity.getChild(XML_FROM);
        Date fromDate = XMLUtils.getDateValue(from, withValidation);
        Element to = activity.getChild(XML_TO);
        Date toDate = XMLUtils.getDateValue(to, withValidation); // can be null
        String requestType = activity.getChildText(XML_REQUESTTYPE);

        if (requestType.equals("EOU")) { // calculate duration
            toDate = XMLUtils.getDateValue(to, withValidation);
            XMLUtils.setDurationValue(durationElem, toDate.getTime() - fromDate.getTime());
        } else if (fromDate == null) {
            if (toDate != null) {
                fromDate = new Date(toDate.getTime());
                if (duration != null) {
                    duration.negate().addTo(fromDate);
                    XMLUtils.setDateValue(from, fromDate);
                }
            } else if (defaultDuration != null) {
                defaultDuration.negate().addTo(fromDate);
                XMLUtils.setDateValue(from, fromDate);
            }
        } else {
            toDate = new Date(fromDate.getTime());
            if (duration != null) {
                duration.addTo(toDate); // TODO@tbe: if very very long duration, to is
                                        // one hour to high
                XMLUtils.setDateValue(to, toDate);
            } else if (defaultDuration != null) {
                defaultDuration.addTo(toDate);
                XMLUtils.setDateValue(to, toDate);
            }
        }

        activityNamesProcessed.add(activityName);
        logger.debug(activityName + ", set from: " + from.getText() + ", to: " + to.getText() + ", duration: "
                + durationElem.getText());

        // set times of following activities
        String xpath = XMLUtils.getXPATH_ActivityElement(activityName, XML_UTILISATIONREL, null);
        List relations = XMLUtils.getXMLObjects(rup, xpath);
        for (Object o : relations) {
            Element relation = (Element) o;
            String otherActivityName = relation.getChildText(XML_OTHERACTIVITYNAME);
            if (activityNamesProcessed.contains(otherActivityName)) {
                continue; // activity has been processed already
            }

            Duration min = XMLUtils.getDurationValue(relation.getChild(XML_MIN), withValidation);
            List otherActivities = XMLUtils.getXMLObjects(rup, XMLUtils.getXPATH_Activities(otherActivityName));
            for (Object obj : otherActivities) {
                Element otherActivity = (Element) obj;
                Date thisDate;
                if (relation.getChildText(XML_THISUTILISATIONTYPE).equals(UTILISATION_TYPE_BEGIN)) {
                    thisDate = fromDate;
                } else if (duration != null) {
                    thisDate = toDate;
                } else {
                    continue;
                }
                min.addTo(thisDate); // add time gap between activities

                if (relation.getChildText(XML_OTHERUTILISATIONTYPE).equals(UTILISATION_TYPE_END)) {
                    Duration otherDuration = XMLUtils.getDurationValue(otherActivity.getChild(XML_DURATION),
                            withValidation);
                    (otherDuration == null ? defaultDuration : otherDuration).negate().addTo(thisDate);
                }
                XMLUtils.setDateValue(otherActivity.getChild(XML_FROM), thisDate);

                setTimes(rup, otherActivity, withValidation, activityNamesProcessed, defaultDuration);
            }
        }

        // set time of previous activities if this activity is not started
        if (!requestType.equals("POU")) {
            return;
        }
        xpath = XMLUtils.getXPATH_ActivityElement(null, XML_UTILISATIONREL, null);
        xpath += "[" + XML_OTHERACTIVITYNAME + "/text()='" + activityName + "']";
        relations = XMLUtils.getXMLObjects(rup, xpath);
        for (Object o : relations) {
            Element relation = (Element) o;
            Element otherActivity = relation.getParentElement();
            if (activityNamesProcessed.contains(otherActivity.getChildText(XML_ACTIVITYNAME))) {
                continue; // activity has been processed already
            }

            Duration min = XMLUtils.getDurationValue(relation.getChild(XML_MIN), withValidation);
            Date otherDate;
            if (relation.getChildText(XML_OTHERUTILISATIONTYPE).equals(UTILISATION_TYPE_BEGIN)) {
                fromDate = XMLUtils.getDateValue(from, withValidation);
                otherDate = fromDate;
            } else if (duration != null) {
                toDate = XMLUtils.getDateValue(to, withValidation);
                otherDate = toDate;
            } else {
                continue;
            }
            min.negate().addTo(otherDate);

            if (relation.getChildText(XML_THISUTILISATIONTYPE).equals(UTILISATION_TYPE_END)) {
                Duration otherDuration = XMLUtils.getDurationValue(otherActivity.getChild(XML_DURATION),
                        withValidation);
                (otherDuration == null ? defaultDuration : otherDuration).negate().addTo(otherDate);
            }
            XMLUtils.setDateValue(otherActivity.getChild(XML_FROM), otherDate);

            setTimes(rup, otherActivity, withValidation, activityNamesProcessed, defaultDuration);
        }
    }

    /**
     * find time slot for rup
     * 
     * @param rup
     */
    public boolean findTimeSlot(Document rup, boolean withValidation) {
        String caseId = XMLUtils.getCaseId(rup);
        logger.debug("reschedule caseId " + caseId);
        boolean isRescheduledRUP = false;

        try {
            // Map with activities and their list of possible offsets for adding to
            // FROM
            Map<String, List<List<Long>>> actOffsets = new HashMap<String, List<List<Long>>>();
            Date latestTO = null;

            // get availabilities for all resources of rup between from and to
            String xpath = XMLUtils.getXPATH_Activities();
            List<Element> activities = XMLUtils.getXMLObjects(rup, xpath);
            for (Element activity : activities) {
                String activityName = activity.getChildText(XML_ACTIVITYNAME);
                // logger.debug("activity name: " + activityName);
                // logger.debug("activity: " + Utils.object2String(activity));
                List<Element> reservations = activity.getChildren(XML_RESERVATION);
                if (reservations.isEmpty()) {
                    continue;
                }

                actOffsets.put(activityName, new ArrayList<List<Long>>());
                Date from = XMLUtils.getDateValue(activity.getChild(XML_FROM), withValidation);
                Date to = XMLUtils.getDateValue(activity.getChild(XML_TO), withValidation);

                // compute latest date to check for free timeslots
                // between earliest FROM date (of first activity) and
                // maxTimeslotPeriod hours later
                if (latestTO == null) {
                    Calendar cal = Calendar.getInstance();
                    /*
                     * latestTO = Utils.string2Date(Utils.date2String(to,
                     * Utils.DATE_PATTERN_XML), Utils.DATE_PATTERN_XML);
                     * cal.setTime(latestTO); cal.roll(Calendar.DAY_OF_MONTH, 1);
                     * latestTO = cal.getTime();
                     */
                    Integer maxTimeslotPeriod = PropertyReader.getInstance()
                            .getIntProperty(PropertyReader.SCHEDULING, "maxTimeslotPeriod");
                    cal.setTime(from);
                    cal.add(Calendar.HOUR_OF_DAY, maxTimeslotPeriod);
                    // cal.roll(Calendar.HOUR_OF_DAY, maxTimeslotPeriod);
                    latestTO = cal.getTime();
                    // logger.debug("latestTO set to " + Utils.date2String(latestTO,
                    // Utils.DATETIME_PATTERN));
                }

                long duration = to.getTime() - from.getTime();
                for (Element reservation : reservations) {
                    Element resource = null;
                    try {
                        resource = reservation.getChild(XML_RESOURCE);
                        int workload = XMLUtils.getIntegerValue(reservation.getChild(XML_WORKLOAD), withValidation);
                        List<Element> timeslots = rs.getAvailabilities(resource, from, latestTO);
                        for (int i = 0; i < timeslots.size(); i++) {
                            Element timeslot = timeslots.get(i);
                            Date tsFrom = XMLUtils.getDateValue(timeslot.getChild("start"), withValidation);
                            Date tsTo = XMLUtils.getDateValue(timeslot.getChild("end"), withValidation);
                            long tsDuration = tsTo.getTime() - tsFrom.getTime();
                            long offsetMin = tsFrom.getTime() - from.getTime();
                            long offsetMax = tsTo.getTime() - to.getTime();
                            int tsAvailability = XMLUtils.getIntegerValue(timeslot.getChild("availability"),
                                    withValidation);
                            int tsAvailabilityDiff = tsAvailability - workload;

                            // try to pass activity into timeslot, if possible, add min
                            // and max offset
                            if (tsAvailabilityDiff >= 0 && duration <= tsDuration && offsetMin >= 0
                                    && offsetMax >= 0) {
                                List<Long> offset = new ArrayList<Long>();
                                offset.add(offsetMin);
                                offset.add(offsetMax);
                                List<List<Long>> offsets = actOffsets.get(activityName);
                                offsets.add(offset);
                                // logger.debug("insert offset for "+activityName+", timeslot "+i+": "
                                // + Utils.toString(offset));
                                // TODO@tbe: ohne Speicherung des verbrauchten workloads
                                // (nur relevant wenn gleiche Ressource mehrfach pro
                                // Aktivitt)
                                // --> verbrauchte availabiliy (tsAvailabilityDiff) am
                                // offset speichern und beachten
                            }
                        }
                    } catch (Exception e) {
                        logger.error("error get availability for resource:" + Utils.element2String(resource, true),
                                e);
                    }
                }
            }
            // logger.debug("offsets: " + Utils.toString(actOffsets));

            // get average of offsets of each activity
            List<List<Long>> offsetsAvg = new ArrayList<List<Long>>();
            for (String activityName : actOffsets.keySet()) {
                List<List<Long>> offsets = actOffsets.get(activityName);
                if (offsetsAvg.isEmpty()) {
                    offsetsAvg.addAll(offsets);
                } else {
                    xpath = XMLUtils.getXPATH_Activities(activityName);
                    Element activity = (Element) XMLUtils.getXMLObjects(rup, xpath).get(0);
                    Date from = XMLUtils.getDateValue(activity.getChild(XML_FROM), withValidation);
                    Date to = XMLUtils.getDateValue(activity.getChild(XML_TO), withValidation);
                    long duration = to.getTime() - from.getTime();

                    List<List<Long>> offsetsAvgNew = new ArrayList<List<Long>>();
                    for (List<Long> offset : offsets) {
                        for (List<Long> offsetAvg : offsetsAvg) {
                            Long offsetMin = Math.max(offset.get(0), offsetAvg.get(0));
                            Long offsetMax = Math.min(offset.get(1), offsetAvg.get(1));
                            long offsetDuration = offsetMax - offsetMin;
                            if (offsetDuration >= duration) {
                                List<Long> offsetAvgNew = new ArrayList<Long>();
                                offsetAvgNew.add(offsetMin);
                                offsetAvgNew.add(offsetMax);
                                offsetsAvgNew.add(offsetAvgNew);
                            }
                        }
                    }
                    offsetsAvg = offsetsAvgNew;
                }
            }
            logger.debug("caseId: " + caseId + ", offsetsAvg: " + Utils.toString(offsetsAvg));

            // add min of first offset to each activity
            if (offsetsAvg.isEmpty()) {
                logger.error("no timeslot available for case: " + caseId);
                XMLUtils.addErrorValue(rup.getRootElement(), withValidation, "msgTimeslotUnavailable");
                isRescheduledRUP = true;
            } else {
                long offsetAvgMin = Long.MAX_VALUE;
                for (List<Long> offsetAvg : offsetsAvg) {
                    offsetAvgMin = Math.min(offsetAvgMin, offsetAvg.get(0));
                }
                logger.info("timeslot for case: " + caseId + " found, offset=" + offsetAvgMin + " ("
                        + Utils.date2String(new Date(offsetAvgMin), Utils.TIME_PATTERN, TimeZone.getTimeZone("GMT"))
                        + " h)");
                if (offsetAvgMin > 0) {
                    for (Element activity : activities) {
                        String activityName = activity.getChildText(XML_ACTIVITYNAME);

                        Element from = activity.getChild(XML_FROM);
                        Date fromDate = XMLUtils.getDateValue(from, false);
                        fromDate = new Date(fromDate.getTime() + offsetAvgMin);
                        XMLUtils.setDateValue(from, fromDate);
                        // logger.debug("set "+activityName+" from: " +
                        // Utils.date2String(fromDate, Utils.DATETIME_PATTERN));

                        Element to = activity.getChild(XML_TO);
                        Date toDate = XMLUtils.getDateValue(to, false);
                        toDate = new Date(toDate.getTime() + offsetAvgMin);
                        XMLUtils.setDateValue(to, toDate);
                        // logger.debug("set "+activityName+" to: " +
                        // Utils.date2String(toDate, Utils.DATETIME_PATTERN));
                        isRescheduledRUP = true;
                    }
                }
            }
        } catch (Exception e) {
            logger.error("cannot found timeslot for case: " + caseId, e);
            XMLUtils.addErrorValue(rup.getRootElement(), withValidation, "msgTimeslotError", e.getMessage());
        }

        return isRescheduledRUP;
    }

    /**
     * findet potentiell kollidierende RUPs, welche definiert sind als: alle RUPs
     * welche die selben Ressourcen zur gleichen Zeit definieren wie der
     * vorhergehend RUP. als selbe Ressourcen werden nur nicht austauschbare
     * Ressourcen betrachtet, das sind Ressourcen mit Reservierung by Id
     * (TODO@tbe: Reservierungen by Role oder Category knnten auch kollidieren,
     * hier aber erstmal weglassen) TODO@tbe: nur Aktivitten beachten, welche
     * Reservierungen haben
     * 
     * @param rup
     * @return
     */
    private List<Document> getCollidingRups(Document rup, List<String> excludedCaseIds) {

        // find earliest FROM and latest TO time of rup
        Date earliestBeginDate = XMLUtils.getEarliestBeginDate(rup);
        Date latestEndDate = XMLUtils.getLatestEndDate(rup);

        List<Case> collidingCases = dataMapper.getRupsByInterval(earliestBeginDate, latestEndDate, excludedCaseIds,
                true);
        List<Document> collidingRups = SchedulingService.getInstance().getRupList(collidingCases);

        if (debug) {
            List<String> caseIds = new ArrayList<String>();
            for (Document collidingRUP : collidingRups) {
                caseIds.add(XMLUtils.getCaseId(collidingRUP));
            }
            logger.debug("found " + collidingRups.size() + " potential colliding rups: " + Utils.getCSV(caseIds));
        }
        return collidingRups;
    }

}