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}