001package jmri;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.util.ArrayList;
006import java.util.IdentityHashMap;
007import java.util.List;
008import java.util.Objects;
009
010import javax.annotation.CheckForNull;
011import javax.annotation.CheckReturnValue;
012import javax.annotation.Nonnull;
013
014import jmri.managers.AbstractManager;
015
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Instance for controlling the issuing of NamedBeanHandles.
021 * <hr>
022 * The NamedBeanHandleManager, deals with controlling and updating {@link NamedBean} objects
023 * across JMRI. When a piece of code requires persistent access to a bean, it
024 * should use a {@link NamedBeanHandle}. The {@link NamedBeanHandle} stores not only the bean
025 * that has been requested but also the named that was used to request it
026 * (either User or System Name).
027 * <p>
028 * This Manager will only issue out one {@link NamedBeanHandle} per Bean/Name request.
029 * The Manager also deals with updates and changes to the names of {@link NamedBean} objects, along
030 * with moving usernames between different beans.
031 * <p>
032 * If a beans username is changed by the user, then the name will be updated in
033 * the NamedBeanHandle. If a username is moved from one bean to another, then
034 * the bean reference will be updated and the propertyChangeListener attached to
035 * that bean will also be moved, so long as the correct method of adding the
036 * listener has been used.
037 * <hr>
038 * This file is part of JMRI.
039 * <p>
040 * JMRI is free software; you can redistribute it and/or modify it under the
041 * terms of version 2 of the GNU General Public License as published by the Free
042 * Software Foundation. See the "COPYING" file for a copy of this license.
043 * <p>
044 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
045 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
046 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
047 *
048 * @see jmri.NamedBean
049 * @see jmri.NamedBeanHandle
050 *
051 * @author Kevin Dickerson Copyright (C) 2011
052 */
053public class NamedBeanHandleManager extends AbstractManager<NamedBean> implements InstanceManagerAutoDefault {
054
055    public NamedBeanHandleManager() {
056        // use Internal memo as connection for this manager
057        super();
058    }
059
060    @SuppressWarnings("unchecked") // namedBeanHandles contains multiple types of NameBeanHandles<T>
061    @Nonnull
062    @CheckReturnValue
063    public <T extends NamedBean> NamedBeanHandle<T> getNamedBeanHandle(@Nonnull String name, @Nonnull T bean) {
064        Objects.requireNonNull(bean, "bean must be nonnull");
065        Objects.requireNonNull(name, "name must be nonnull");
066        if (name.isEmpty()) {
067            throw new IllegalArgumentException("name cannot be empty in getNamedBeanHandle");
068        }
069        synchronized (namedBeanHandles) {
070            List<NamedBeanHandle<?>> handles = namedBeanHandles.computeIfAbsent(bean, b -> new ArrayList<>(2));
071            for (NamedBeanHandle<?> h : handles) {
072                if (name.equals(h.getName())) {
073                    return (NamedBeanHandle<T>) h;
074                }
075            }
076            NamedBeanHandle<T> temp = new NamedBeanHandle<>(name, bean);
077            handles.add(temp);
078            return temp;
079        }
080    }
081
082    /**
083     * Update the name of a bean in its references.
084     * <p>
085     * <strong>Note</strong> this does not change the name on the bean, it only
086     * changes the references.
087     *
088     * @param <T>     the type of the bean
089     * @param oldName the name changing from
090     * @param newName the name changing to
091     * @param bean    the bean being renamed
092     */
093    @SuppressWarnings("unchecked") // namedBeanHandles contains multiple types of NameBeanHandles<T>
094    public <T extends NamedBean> void renameBean(@Nonnull String oldName, @Nonnull String newName, @Nonnull T bean) {
095
096        /*Gather a list of the beans in the system with the oldName ref.
097         Although when we get a new bean we always return the first one that exists,
098         when a rename is performed it doesn't delete the bean with the old name;
099         it simply updates the name to the new one. So hence you can end up with
100         multiple named bean entries for one name.
101         */
102        synchronized (namedBeanHandles) {
103            for (NamedBeanHandle<?> h : namedBeanHandles.getOrDefault(bean, List.of())) {
104                if (oldName.equals(h.getName())) {
105                    h.setName(newName);
106                }
107            }
108        }
109        updateListenerRef(oldName, newName, bean);
110    }
111
112    /**
113     * Effectively move a name from one bean to another.
114     * <p>
115     * <strong>Note</strong> only updates the references to point to the new
116     * bean; does not move the name provided from one bean to another.
117     *
118     * @param <T>     the bean type
119     * @param oldBean bean loosing the name
120     * @param name    name being moved
121     * @param newBean bean gaining the name
122     */
123    //Checks are performed to make sure that the beans are the same type before being moved
124    @SuppressWarnings("unchecked") // namedBeanHandles contains multiple types of NameBeanHandles<T>
125    public <T extends NamedBean> void moveBean(@Nonnull T oldBean, @Nonnull T newBean, @Nonnull String name) {
126        /*Gather a list of the beans in the system with the oldBean ref.
127         Although when a new bean is requested, we always return the first one that exists
128         when a move is performed it doesn't delete the namedbeanhandle with the oldBean
129         it simply updates the bean to the new one. So hence you can end up with
130         multiple bean entries with the same name.
131         */
132
133        synchronized (namedBeanHandles) {
134            List<NamedBeanHandle<?>> oldHandles = namedBeanHandles.get(oldBean);
135            if (oldHandles != null && oldBean != newBean) {
136                List<NamedBeanHandle<?>> moved = new ArrayList<>();
137                for (NamedBeanHandle<?> h : oldHandles) {
138                    if (name.equals(h.getName())) {
139                        ((NamedBeanHandle<T>) h).setBean(newBean);
140                        moved.add(h);
141                    }
142                }
143                oldHandles.removeAll(moved);
144                if (oldHandles.isEmpty()) {
145                    namedBeanHandles.remove(oldBean);
146                }
147                if (!moved.isEmpty()) {
148                    namedBeanHandles.computeIfAbsent(newBean, b -> new ArrayList<>(2)).addAll(moved);
149                }
150            }
151        }
152        moveListener(oldBean, newBean, name);
153    }
154
155    public void updateBeanFromUserToSystem(@Nonnull NamedBean bean) {
156        String systemName = bean.getSystemName();
157        String userName = bean.getUserName();
158        if (userName == null) {
159            log.warn("updateBeanFromUserToSystem requires non-blank user name: \"{}\" not renamed", systemName);
160            return;
161        }
162        renameBean(userName, systemName, bean);
163    }
164
165    public void updateBeanFromSystemToUser(@Nonnull NamedBean bean) throws JmriException {
166        String userName = bean.getUserName();
167        String systemName = bean.getSystemName();
168
169        if ((userName == null) || (userName.equals(""))) {
170            log.error("UserName is empty, can not update items to use UserName");
171            throw new JmriException("UserName is empty, can not update items to use UserName");
172        }
173        renameBean(systemName, userName, bean);
174    }
175
176    @CheckReturnValue
177    public <T extends NamedBean> boolean inUse(@Nonnull String name, @Nonnull T bean) {
178        synchronized (namedBeanHandles) {
179            return namedBeanHandles.getOrDefault(bean, List.of()).stream()
180                    .anyMatch((h) -> (name.equals(h.getName())));
181        }
182    }
183
184    /**
185     * Forget all the handles for a bean that is being disposed of, so that
186     * the handles and the bean can be released.
187     *
188     * @param bean the bean being disposed of
189     */
190    public void removeNamedBeanHandles(@Nonnull NamedBean bean) {
191        synchronized (namedBeanHandles) {
192            namedBeanHandles.remove(bean);
193        }
194    }
195
196    @CheckForNull
197    @CheckReturnValue
198    public <T extends NamedBean> NamedBeanHandle<T> newNamedBeanHandle(@Nonnull String name, @Nonnull T bean, @Nonnull Class<T> type) {
199        return getNamedBeanHandle(name, bean);
200    }
201
202    /**
203     * A method to update the listener reference from oldName to a newName
204     */
205    private void updateListenerRef(@Nonnull String oldName, @Nonnull String newName, @Nonnull NamedBean nBean) {
206        java.beans.PropertyChangeListener[] listeners = nBean.getPropertyChangeListenersByReference(oldName);
207        for (java.beans.PropertyChangeListener listener : listeners) {
208            nBean.updateListenerRef(listener, newName);
209        }
210    }
211
212    /**
213     * Moves a propertyChangeListener from one bean to another, where the
214     * listener reference matches the currentName.
215     */
216    private void moveListener(@Nonnull NamedBean oldBean, @Nonnull NamedBean newBean, @Nonnull String currentName) {
217        java.beans.PropertyChangeListener[] listeners = oldBean.getPropertyChangeListenersByReference(currentName);
218        for (java.beans.PropertyChangeListener l : listeners) {
219            String listenerRef = oldBean.getListenerRef(l);
220            oldBean.removePropertyChangeListener(l);
221            newBean.addPropertyChangeListener(l, currentName, listenerRef);
222        }
223    }
224
225    @Override
226    public void dispose() {
227        super.dispose();
228    }
229
230    // the handles for each bean, found by the bean object itself;
231    // namedBeanHandles contains multiple types of NameBeanHandles<T>
232    final IdentityHashMap<NamedBean, List<NamedBeanHandle<?>>> namedBeanHandles = new IdentityHashMap<>();
233
234    /**
235     * Don't want to store this information
236     */
237    @Override
238    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
239            justification = "This method intentionally doesn't do anything")
240    protected void registerSelf() {
241    }
242
243    @Override
244    @CheckReturnValue
245    public char typeLetter() {
246        throw new UnsupportedOperationException("Not supported yet.");
247    }
248
249    @Override
250    @Nonnull
251    @CheckReturnValue
252    public String makeSystemName(@Nonnull String s) {
253        throw new UnsupportedOperationException("Not supported yet.");
254    }
255
256    @Override
257    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
258            justification = "This method must never be called")
259    public void register(@Nonnull NamedBean n) {
260        throw new UnsupportedOperationException("Not supported yet.");
261    }
262
263    @Override
264    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
265            justification = "This method must never be called")
266    public void deregister(@Nonnull NamedBean n) {
267        throw new UnsupportedOperationException("Not supported yet.");
268    }
269
270    @Override
271    @CheckReturnValue
272    public int getXMLOrder() {
273        throw new UnsupportedOperationException("Not supported yet.");
274    }
275
276    @Override
277    @Nonnull
278    @CheckReturnValue
279    public String getBeanTypeHandled(boolean plural) {
280        return Bundle.getMessage(plural ? "BeanNames" : "BeanName");
281    }
282
283    /**
284     * {@inheritDoc}
285     */
286    @Override
287    public Class<NamedBean> getNamedBeanClass() {
288        return NamedBean.class;
289    }
290
291    private static final Logger log = LoggerFactory.getLogger(NamedBeanHandleManager.class);
292
293}