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}