001package jmri.jmrit.ctc;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.Collections;
006import java.util.HashSet;
007import java.util.concurrent.locks.ReentrantLock;
008import jmri.Sensor;
009
010/**
011 * Each of these objects describe a route consisting of all of the
012 * occupancy sensors(s) that specify a route.  This is the "topology"
013 * information needed to determine if a route is available or not.
014 * <p>
015 * For O.S. sections, this routine will get the reference to the
016 * "_mOSSectionOccupiedExternalSensor" associated with that O.S. section.  For
017 * the occupancy sensors, just a reference to those occupancy sensors.
018 * <p>
019 * It will register with the occupancy sensors for state change.  As
020 * each occupancy sensor goes unoccupied (INACTIVE), it will subtract one from
021 * a counter of outstanding reservations.  If the count becomes zero,
022 * it will remove from it's list that resource, thereby freeing it up for any
023 * other allocation in the future.
024 * <p>
025 * This object is NOT multi-thread safe (nor does it need to be).
026 * I DO NOT (nor do I make any assumptions) about JMRI's threading as regards
027 * this objects design.
028 * It is "hardened" against multi-threading issues of ONLY this form:
029 * It is possible that while we are (say) adding a new route by merging it
030 * to an existing route, for an de-occupancy event to occur.  If we didn't
031 * harden the object, then it is possible for an increment and decrement
032 * to occur "in parallel" thus screwing up the count.
033 * So whenever a merge or de-occupy event occurs, we lock down the object
034 * "CountedSensor" only as long as needed to get to a "safe" state.
035 * 
036 * @author Gregory J. Bedlek Copyright (C) 2018, 2019, 2020
037 * 
038*/
039
040public class LockedRoute {
041    
042/**
043 * This class implements the counted sensor concept.  When a CountedSensor is created,
044 * the count is set to 1.  If another allocation occurs of the same sensor,
045 * the count is increased by 1.  When a de-allocation occurs, the count is
046 * decreased by one.  IF the count goes to zero, the object is deleted.
047 * Both of these increment / decrement operations are atomic, i.e. protected
048 * by locks.
049 */    
050    private static class CountedSensor {
051        private final Sensor        _mSensor;
052        private int                 _mCount;
053        private final ReentrantLock _mLock = new ReentrantLock();
054        private CountedSensor(Sensor sensor) {
055            _mSensor = sensor;
056            _mCount = 1;
057        }
058        private Sensor getSensor() { return _mSensor; }
059        private void lockedIncrementCount() {
060            _mLock.lock();
061            _mCount++;
062            _mLock.unlock();
063        }
064        private boolean lockedDecrementCountAndCheckIfZero() {
065            boolean returnValue;
066            _mLock.lock();
067            _mCount--;                  // Can't throw
068            returnValue = _mCount == 0; // Ditto
069            _mLock.unlock();
070            return returnValue;
071        }
072        private String dumpIt() { return _mSensor.getDisplayName() + "(" + String.valueOf(_mCount) + ")"; }
073    }
074    
075    private HashSet<CountedSensor> getCountedSensorHashSet(HashSet<Sensor> sensors) {
076        HashSet<CountedSensor> returnValue = new HashSet<>();
077        sensors.forEach((sensor) -> {
078            returnValue.add(new CountedSensor(sensor));
079        });
080        return returnValue;
081    }
082    
083    private final LockedRoutesManager _mLockedRoutesManager;
084    private final String _mOSSectionDescription;                // For debugging
085    private final String _mRuleDescription;                     // Ditto
086    private final HashSet<CountedSensor> _mCountedSensors;
087    private final boolean _mRightTraffic;
088    private final PropertyChangeListener _mSensorPropertyChangeListener = (PropertyChangeEvent e) -> { occupancyStateChange(e); };
089    
090    public LockedRoute(LockedRoutesManager lockedRoutesManager, HashSet<Sensor> sensors, String osSectionDescription, String ruleDescription, boolean rightTraffic) {
091        _mLockedRoutesManager = lockedRoutesManager;    // Reference to our parent.
092        _mOSSectionDescription = osSectionDescription;
093        _mRuleDescription = ruleDescription;
094        _mCountedSensors = getCountedSensorHashSet(sensors);
095        _mRightTraffic = rightTraffic;
096    }
097    public HashSet<CountedSensor> getCountedSensors() { return _mCountedSensors; }
098    
099    public HashSet<Sensor> getSensors() {
100        HashSet<Sensor> returnValue = new HashSet<>();
101        _mCountedSensors.forEach((countedSensor) -> {
102            returnValue.add(countedSensor.getSensor());
103        });
104        return returnValue;
105    }
106    
107    /**
108     * This routine is ONLY called by the higher level if this is NOT a merged route,
109     * but a brand new route.
110     * <p>
111     * Once the higher level has determined that this route is valid and available,
112     * then finish the process here.  Here we register occupancy changes for all
113     * sensors, and as they report "unoccupied", we prune that entry from our
114     * set, thereby releasing that segment to the rest of the system.  See
115     * "occupancyStateChange".
116     */
117    public void allocateRoute() {
118        _mCountedSensors.forEach((countedSensor) -> {
119            countedSensor.getSensor().addPropertyChangeListener(_mSensorPropertyChangeListener);
120        });
121    }
122    
123    /**
124     * The higher level called us to merge "newLockedRoute" sensors into "this" object.
125     * "newLockedRoute" has had NOTHING done to it other than construct it.
126     * We iterate the sensors in it, and if there is a match in our sensor list
127     * just increment the count, otherwise register a property change on the new added sensor.
128     * <p>
129     * After this, "newLockedRoute" is not needed for anything.
130     * <p>
131     * NOTE: The higher level routines saved a copy of "this" object somewhere,
132     * so that when Signals Normal (all stop) is ever selected by the Dispatcher,
133     * we can delete this entire object easily.  Ergo, we MUST merge the newLockedRoute
134     * into "this", NOT the other way around.  Besides, merge to "this" is easier
135     * anyways.....
136     * @param newLockedRoute New route that needs to be merged to "this".
137     */
138    public void mergeRoutes(LockedRoute newLockedRoute) {
139        HashSet<CountedSensor> newCountedSensors = newLockedRoute.getCountedSensors();
140        for (CountedSensor newCountedSensor : newCountedSensors) { // Do the new HashSet first:
141            boolean foundMatch = false;
142            for (CountedSensor thisCountedSensor : _mCountedSensors) { // For all in "this"
143                if (newCountedSensor.getSensor() == thisCountedSensor.getSensor()) { // Match, increment usage count:
144                    thisCountedSensor.lockedIncrementCount();
145                    foundMatch = true;
146                    break;
147                }
148            }
149            if (!foundMatch) {  // Need to add to our list:
150                _mCountedSensors.add(newCountedSensor); // Ok to add, not in loop referencing this HashSet!
151                newCountedSensor.getSensor().addPropertyChangeListener(_mSensorPropertyChangeListener);
152            }
153        }
154    }
155
156    public enum AnyInCommonReturn { NONE,       // NOTHING matches at all.
157                                    YES,        // Absolute overlap.
158                                    FLEETING }  // One overlaps, we were asked to checkDirection, and the direction matches
159    /**
160     * Checks the sensors passed in as object type "LockedRoute" against the sensors in this object.
161     * Support Fleeting requests.
162     * 
163     * @param lockedRoute Existing LockedRoute to check against.
164     * @param checkDirection Pass false if this is a turnout resource request, else pass true to check traffic direction.
165     * @param rightTraffic If right traffic was requested pass true, else false for left traffic.
166     * @return AnyInCommonReturn enum value.  NONE = Nothing matches at all, YES = Absolute overlap, FLEETING = Overlap, but direction matches.
167     */
168    public AnyInCommonReturn anyInCommon(LockedRoute lockedRoute, boolean checkDirection, boolean rightTraffic) {
169        //_mCountedSensors.
170        boolean anyInCommon = !Collections.disjoint(getSensors(), lockedRoute.getSensors());
171        if (anyInCommon) { // We need to check to see if it is a "fleeting" operations:
172            if (checkDirection && rightTraffic == lockedRoute._mRightTraffic) return AnyInCommonReturn.FLEETING; // If we need to check direction, AND we are in the same direction, no problem.  Fleeting of some sort.
173        }
174        return anyInCommon ? AnyInCommonReturn.YES : AnyInCommonReturn.NONE;
175    }
176    
177    /**
178     * Simple routine to remove all listeners that were registered to each of our counted sensors.
179     */
180    public void removeAllListeners() {
181        _mCountedSensors.forEach((countedSensor) -> {
182            countedSensor.getSensor().removePropertyChangeListener(_mSensorPropertyChangeListener);
183        });
184    }
185    
186    /**
187     * Simple routine to return in a string all information on this route.
188     * 
189     * @return Dump of routes information in returned string.  No specific format.  See code in routine.
190     */
191    public String dumpRoute() {
192        String returnString = "";
193        for (CountedSensor countedSensor : _mCountedSensors) {
194            if (returnString.isEmpty()) returnString = countedSensor.dumpIt();
195            else returnString += ", " + countedSensor.dumpIt();
196        }
197        returnString += _mRightTraffic ? " Dir:R" : " Dir:L";  // NOI18N
198        return "O.S. " + _mOSSectionDescription + _mRuleDescription + " " + Bundle.getMessage("LockedRouteSensorsStillAllocatedList") + " " + returnString; // NOI18N
199    }
200    
201/**
202 * IF the sensor (NOT the NBHSensor!) went inactive (unoccupied), and the count
203 * went to zero, remove it from our allocated resource list:
204 */
205    private void occupancyStateChange(PropertyChangeEvent e) {
206        if (e.getPropertyName().equals("KnownState") && (int)e.getNewValue() == Sensor.INACTIVE) {  // NOI18N  Went inactive, prune us:
207            Sensor sensor = (Sensor)e.getSource();
208            for (CountedSensor countedSensor : _mCountedSensors) {
209                if (countedSensor.getSensor() == sensor) { // Free this resource.
210                    if (countedSensor.lockedDecrementCountAndCheckIfZero()) { // Went to zero, remove:
211                        sensor.removePropertyChangeListener(_mSensorPropertyChangeListener);    // Not watching this one anymore.
212                        _mCountedSensors.remove(countedSensor);
213                    }
214                    break;      // Can be ONLY once in "_mCountedSensors", due to prior merges!
215                }
216            }
217            if (_mCountedSensors.isEmpty()) { // Notify parent that we are empty, so it can purge us completely from it's master list:
218                _mLockedRoutesManager.cancelLockedRoute(this);
219            }
220        }
221    }
222}