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