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