001package jmri;
002
003import java.beans.PropertyChangeEvent;
004import java.io.IOException;
005import java.time.Instant;
006import java.util.ArrayList;
007import java.util.List;
008import javax.annotation.CheckForNull;
009import javax.annotation.CheckReturnValue;
010import javax.annotation.Nonnull;
011
012import jmri.implementation.AbstractShutDownTask;
013import jmri.implementation.SignalSpeedMap;
014import jmri.jmrit.display.layoutEditor.BlockValueFile;
015import jmri.managers.AbstractManager;
016
017/**
018 * Basic implementation of a BlockManager.
019 * <p>
020 * Note that this does not enforce any particular system naming convention.
021 * <p>
022 * Note this is a concrete class, unlike the interface/implementation pairs of
023 * most Managers, because there are currently only one implementation for
024 * Blocks.
025 * <hr>
026 * This file is part of JMRI.
027 * <p>
028 * JMRI is free software; you can redistribute it and/or modify it under the
029 * terms of version 2 of the GNU General Public License as published by the Free
030 * Software Foundation. See the "COPYING" file for a copy of this license.
031 * <p>
032 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
033 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
034 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
035 *
036 * @author Bob Jacobsen Copyright (C) 2006
037 */
038public class BlockManager extends AbstractManager<Block> implements ProvidingManager<Block>, InstanceManagerAutoDefault {
039
040    private final String powerManagerChangeName;
041    public final ShutDownTask shutDownTask = new AbstractShutDownTask("Writing Blocks") {
042        @Override
043        public void run() {
044            try {
045                new BlockValueFile().writeBlockValues();
046            } catch (IOException ex) {
047                log.error("Exception writing blocks", ex);
048            }
049        }
050    };
051
052    public BlockManager() {
053        super();
054        InstanceManager.getDefault(SensorManager.class).addVetoableChangeListener(this);
055        InstanceManager.getDefault(ReporterManager.class).addVetoableChangeListener(this);
056        InstanceManager.getList(PowerManager.class).forEach(pm -> pm.addPropertyChangeListener(this));
057        powerManagerChangeName = InstanceManager.getListPropertyName(PowerManager.class);
058        InstanceManager.addPropertyChangeListener(this);
059        InstanceManager.getDefault(ShutDownManager.class).register(shutDownTask);
060    }
061
062    @Override
063    public void dispose() {
064        super.dispose();
065        InstanceManager.getDefault(ShutDownManager.class).deregister(shutDownTask);
066    }
067
068    @Override
069    @CheckReturnValue
070    public int getXMLOrder() {
071        return Manager.BLOCKS;
072    }
073
074    @Override
075    @CheckReturnValue
076    public char typeLetter() {
077        return 'B';
078    }
079
080    @Override
081    public Class<Block> getNamedBeanClass() {
082        return Block.class;
083    }
084
085    private boolean saveBlockPath = true;
086
087    @CheckReturnValue
088    public boolean isSavedPathInfo() {
089        return saveBlockPath;
090    }
091
092    public void setSavedPathInfo(boolean save) {
093        saveBlockPath = save;
094    }
095
096    /**
097     * Create a new Block, only if it does not exist.
098     *
099     * @param systemName the system name
100     * @param userName   the user name
101     * @return null if a Block with the same systemName or userName already
102     *         exists, or if there is trouble creating a new Block
103     */
104    @CheckForNull
105    public Block createNewBlock(@Nonnull String systemName, @CheckForNull String userName) {
106        // Check that Block does not already exist
107        Block r;
108        if (userName != null && !userName.isEmpty()) {
109            r = getByUserName(userName);
110            if (r != null) {
111                return null;
112            }
113        }
114        r = getBySystemName(systemName);
115        if (r != null) {
116            return null;
117        }
118        // Block does not exist, create a new Block
119        r = new Block(systemName, userName);
120
121        // Keep track of the last created auto system name
122        updateAutoNumber(systemName);
123
124        // save in the maps
125        register(r);
126        try {
127            r.setBlockSpeed("Global"); // NOI18N
128        } catch (JmriException ex) {
129            log.error("Unexpected exception {}", ex.getMessage());
130        }
131        return r;
132    }
133
134    /**
135     * Create a new Block using an automatically incrementing system
136     * name.
137     *
138     * @param userName the user name for the new Block
139     * @return null if a Block with the same systemName or userName already
140     *         exists, or if there is trouble creating a new Block.
141     */
142    @CheckForNull
143    public Block createNewBlock(@CheckForNull String userName) {
144        return createNewBlock(getAutoSystemName(), userName);
145    }
146
147    /**
148     * If the Block exists, return it, otherwise create a new one and return it.
149     * If the argument starts with the system prefix and type letter, usually
150     * "IB", then the argument is considered a system name, otherwise it's
151     * considered a user name and a system name is automatically created.
152     *
153     * @param name the system name or the user name for the block
154     * @return a new or existing Block
155     * @throws IllegalArgumentException if cannot create block or no name supplied; never returns null
156     */
157    @Nonnull
158    public Block provideBlock(@Nonnull String name) {
159        if (name.isEmpty()) {
160            throw new IllegalArgumentException("Could not create block, no name supplied");
161        }
162        Block b = getBlock(name);
163        if (b != null) {
164            return b;
165        }
166        if (name.startsWith(getSystemNamePrefix())) {
167            b = createNewBlock(name, null);
168        } else {
169            b = createNewBlock(name);
170        }
171        if (b == null) {
172            throw new IllegalArgumentException("Could not create block \"" + name + "\"");
173        }
174        return b;
175    }
176
177    /**
178     * Get an existing Block. First looks up assuming that name is a
179     * User Name. If this fails looks up assuming that name is a System Name. If
180     * both fail, returns null.
181     *
182     * @param name the name of an existing block
183     * @return a Block or null if none found
184     */
185    @CheckReturnValue
186    @CheckForNull
187    public Block getBlock(@Nonnull String name) {
188        Block r = getByUserName(name);
189        if (r != null) {
190            return r;
191        }
192        return getBySystemName(name);
193    }
194
195    @CheckReturnValue
196    @CheckForNull
197    public Block getByDisplayName(@Nonnull String key) {
198        // First try to find it in the user list.
199        // If that fails, look it up in the system list
200        Block retv = this.getByUserName(key);
201        if (retv == null) {
202            retv = this.getBySystemName(key);
203        }
204        // If it's not in the system list, go ahead and return null
205        return (retv);
206    }
207
208    private String defaultSpeed = "Normal";
209
210    /**
211     * @param speed the speed
212     * @throws IllegalArgumentException if provided speed is invalid
213     */
214    public void setDefaultSpeed(@Nonnull String speed) {
215        if (defaultSpeed.equals(speed)) {
216            return;
217        }
218
219        try {
220            Float.parseFloat(speed);
221        } catch (NumberFormatException nx) {
222            try {
223                InstanceManager.getDefault(SignalSpeedMap.class).getSpeed(speed);
224            } catch (IllegalArgumentException ex) {
225                throw new IllegalArgumentException("Value of requested default block speed \"" + speed + "\" is not valid", ex);
226            }
227        }
228        String oldSpeed = defaultSpeed;
229        defaultSpeed = speed;
230        firePropertyChange("DefaultBlockSpeedChange", oldSpeed, speed);
231    }
232
233    @CheckReturnValue
234    @Nonnull
235    public String getDefaultSpeed() {
236        return defaultSpeed;
237    }
238
239    @Override
240    @CheckReturnValue
241    @Nonnull
242    public String getBeanTypeHandled(boolean plural) {
243        return Bundle.getMessage(plural ? "BeanNameBlocks" : "BeanNameBlock");
244    }
245
246    /**
247     * Get a list of blocks which the supplied roster entry appears to be
248     * occupying. A block is assumed to contain this roster entry if its value
249     * is the RosterEntry itself, or a string with the entry's id or dcc
250     * address.
251     *
252     * @param re the roster entry
253     * @return list of block system names
254     */
255    @CheckReturnValue
256    @Nonnull
257    public List<Block> getBlocksOccupiedByRosterEntry(@Nonnull BasicRosterEntry re) {
258        List<Block> blockList = new ArrayList<>();
259        getNamedBeanSet().stream().forEach(b -> {
260            if (b != null) {
261                Object obj = b.getValue();
262                if ((obj instanceof BasicRosterEntry && obj == re) ||
263                        obj.toString().equals(re.getId()) ||
264                        obj.toString().equals(re.getDccAddress())) {
265                    blockList.add(b);
266                }
267            }
268        });
269        return blockList;
270    }
271
272
273    private Instant lastTimeLayoutPowerOn; // the most recent time any power manager had a power ON event
274
275    /**
276     * Listen for changes to the power state from any power managers
277     * in use in order to track how long it's been since power was applied
278     * to the layout. This information is used in {@link Block#goingActive()}
279     * when deciding whether to restore a block's last value.
280     *
281     * Also listen for additions/removals or PowerManagers
282     *
283     * @param e the change event
284     */
285
286    @Override
287    public void propertyChange(PropertyChangeEvent e) {
288        super.propertyChange(e);
289        if (jmri.PowerManager.POWER.equals(e.getPropertyName())) {
290            try {
291                PowerManager pm = (PowerManager) e.getSource();
292                if (pm.getPower() == jmri.PowerManager.ON) {
293                    lastTimeLayoutPowerOn = Instant.now();
294                }
295            } catch (NoSuchMethodError xe) {
296                // do nothing
297            }
298        }
299        if (powerManagerChangeName.equals(e.getPropertyName())) {
300            if (e.getNewValue() == null) {
301                // powermanager has been removed
302                PowerManager pm = (PowerManager) e.getOldValue();
303                pm.removePropertyChangeListener(this);
304            } else {
305                // a powermanager has been added
306                PowerManager pm = (PowerManager) e.getNewValue();
307                pm.addPropertyChangeListener(this);
308            }
309        }
310    }
311
312    /**
313     * Get the amount of time since the layout was last powered up,
314     * in milliseconds. If the layout has not been powered up as far as
315     * JMRI knows it returns a very long time indeed.
316     *
317     * @return long int
318     */
319    public long timeSinceLastLayoutPowerOn() {
320        if (lastTimeLayoutPowerOn == null) {
321            return Long.MAX_VALUE;
322        }
323        return Instant.now().toEpochMilli() - lastTimeLayoutPowerOn.toEpochMilli();
324    }
325
326    @Override
327    @Nonnull
328    public Block provide(@Nonnull String name) {
329        return provideBlock(name);
330    }
331
332    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockManager.class);
333
334}