001package jmri.jmrit.roster.swing; 002 003import com.fasterxml.jackson.databind.util.StdDateFormat; 004 005import java.beans.PropertyChangeEvent; 006import java.beans.PropertyChangeListener; 007import java.text.ParseException; 008import java.util.*; 009 010import javax.annotation.CheckForNull; 011import javax.swing.Icon; 012import javax.swing.ImageIcon; 013import javax.swing.JLabel; 014import javax.swing.table.DefaultTableModel; 015 016import jmri.jmrit.roster.Roster; 017import jmri.jmrit.roster.RosterEntry; 018import jmri.jmrit.roster.RosterIconFactory; 019import jmri.jmrit.roster.rostergroup.RosterGroup; 020import jmri.jmrit.roster.rostergroup.RosterGroupSelector; 021 022/** 023 * Table data model for display of Roster variable values. 024 * <p> 025 * Any desired ordering, etc, is handled outside this class. 026 * <p> 027 * The initial implementation doesn't automatically update when roster entries 028 * change, doesn't allow updating of the entries, and only shows some of the 029 * fields. But it's a start.... 030 * 031 * @author Bob Jacobsen Copyright (C) 2009, 2010 032 * @since 2.7.5 033 */ 034public class RosterTableModel extends DefaultTableModel implements PropertyChangeListener { 035 036 public static final int IDCOL = 0; 037 static final int ADDRESSCOL = 1; 038 static final int ICONCOL = 2; 039 static final int DECODERMFGCOL = 3; 040 static final int DECODERFAMILYCOL = 4; 041 static final int DECODERMODELCOL = 5; 042 static final int ROADNAMECOL = 6; 043 static final int ROADNUMBERCOL = 7; 044 static final int MFGCOL = 8; 045 static final int MODELCOL = 9; 046 static final int OWNERCOL = 10; 047 static final int DATEUPDATECOL = 11; 048 public static final int PROTOCOL = 12; 049 public static final int NUMCOL = PROTOCOL + 1; 050 private String rosterGroup = null; 051 boolean editable = false; 052 053 public RosterTableModel() { 054 this(false); 055 } 056 057 public RosterTableModel(boolean editable) { 058 this.editable = editable; 059 Roster.getDefault().addPropertyChangeListener(RosterTableModel.this); 060 setRosterGroup(null); // add prop change listeners to roster entries 061 } 062 063 /** 064 * Create a table model for a Roster group. 065 * 066 * @param group the roster group to show; if null, behaves the same as 067 * {@link #RosterTableModel()} 068 */ 069 public RosterTableModel(@CheckForNull RosterGroup group) { 070 this(false); 071 if (group != null) { 072 this.setRosterGroup(group.getName()); 073 } 074 } 075 076 @Override 077 public void propertyChange(PropertyChangeEvent e) { 078 if (e.getPropertyName().equals(Roster.ADD)) { 079 setRosterGroup(getRosterGroup()); // add prop change listener to new entry 080 fireTableDataChanged(); 081 } else if (e.getPropertyName().equals(Roster.REMOVE)) { 082 fireTableDataChanged(); 083 } else if (e.getPropertyName().equals(Roster.SAVED)) { 084 //TODO This really needs to do something like find the index of the roster entry here 085 if (e.getSource() instanceof RosterEntry) { 086 int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource()); 087 fireTableRowsUpdated(row, row); 088 } else { 089 fireTableDataChanged(); 090 } 091 } else if (e.getPropertyName().equals(RosterGroupSelector.SELECTED_ROSTER_GROUP)) { 092 setRosterGroup((e.getNewValue() != null) ? e.getNewValue().toString() : null); 093 } else if (e.getPropertyName().startsWith("attribute") && e.getSource() instanceof RosterEntry) { // NOI18N 094 int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource()); 095 fireTableRowsUpdated(row, row); 096 } else if (e.getPropertyName().equals(Roster.ROSTER_GROUP_ADDED) && e.getNewValue().equals(rosterGroup)) { 097 fireTableDataChanged(); 098 } 099 } 100 101 @Override 102 public int getRowCount() { 103 return Roster.getDefault().numGroupEntries(rosterGroup); 104 } 105 106 @Override 107 public int getColumnCount() { 108 return NUMCOL + getModelAttributeKeyColumnNames().length; 109 } 110 111 @Override 112 public String getColumnName(int col) { 113 switch (col) { 114 case IDCOL: 115 return Bundle.getMessage("FieldID"); 116 case ADDRESSCOL: 117 return Bundle.getMessage("FieldDCCAddress"); 118 case DECODERMFGCOL: 119 return Bundle.getMessage("FieldDecoderMfg"); 120 case DECODERFAMILYCOL: 121 return Bundle.getMessage("FieldDecoderFamily"); 122 case DECODERMODELCOL: 123 return Bundle.getMessage("FieldDecoderModel"); 124 case MODELCOL: 125 return Bundle.getMessage("FieldModel"); 126 case ROADNAMECOL: 127 return Bundle.getMessage("FieldRoadName"); 128 case ROADNUMBERCOL: 129 return Bundle.getMessage("FieldRoadNumber"); 130 case MFGCOL: 131 return Bundle.getMessage("FieldManufacturer"); 132 case ICONCOL: 133 return Bundle.getMessage("FieldIcon"); 134 case OWNERCOL: 135 return Bundle.getMessage("FieldOwner"); 136 case DATEUPDATECOL: 137 return Bundle.getMessage("FieldDateUpdated"); 138 case PROTOCOL: 139 return Bundle.getMessage("FieldProtocol"); 140 default: 141 return getColumnNameAttribute(col); 142 } 143 } 144 145 private String getColumnNameAttribute(int col) { 146 if ( col < getColumnCount() ) { 147 String attributeKey = getAttributeKey(col); 148 try { 149 return Bundle.getMessage(attributeKey); 150 } catch (java.util.MissingResourceException ex){} 151 152 String[] r = attributeKey.split("(?=\\p{Lu})"); // NOI18N 153 StringBuilder sb = new StringBuilder(); 154 sb.append(r[0].trim()); 155 for (int j = 1; j < r.length; j++) { 156 sb.append(" "); 157 sb.append(r[j].trim()); 158 } 159 return sb.toString(); 160 } 161 return "<UNKNOWN>"; // NOI18N 162 } 163 164 @Override 165 public Class<?> getColumnClass(int col) { 166 switch (col) { 167 case ADDRESSCOL: 168 return Integer.class; 169 case ICONCOL: 170 return ImageIcon.class; 171 case DATEUPDATECOL: 172 return Date.class; 173 default: 174 return getColumnClassAttribute(col); 175 } 176 } 177 178 private Class<?> getColumnClassAttribute(int col){ 179 if (RosterEntry.ATTRIBUTE_LAST_OPERATED.equals( getAttributeKey(col))) { 180 return Date.class; 181 } 182 if (RosterEntry.ATTRIBUTE_OPERATING_DURATION.equals( getAttributeKey(col))) { 183 return Integer.class; 184 } 185 return String.class; 186 } 187 188 /** 189 * {@inheritDoc} 190 * <p> 191 * Note that the table can be set to be non-editable when constructed, in 192 * which case this always returns false. 193 * 194 * @return true if cell is editable in roster entry model and table allows 195 * editing 196 */ 197 @Override 198 public boolean isCellEditable(int row, int col) { 199 if (col == ADDRESSCOL) { 200 return false; 201 } 202 if (col == PROTOCOL) { 203 return false; 204 } 205 if (col == DECODERMFGCOL) { 206 return false; 207 } 208 if (col == DECODERFAMILYCOL) { 209 return false; 210 } 211 if (col == DECODERMODELCOL) { 212 return false; 213 } 214 if (col == ICONCOL) { 215 return false; 216 } 217 if (col == DATEUPDATECOL) { 218 return false; 219 } 220 if (editable) { 221 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 222 if (re != null) { 223 return (!re.isOpen()); 224 } 225 } 226 return editable; 227 } 228 229 RosterIconFactory iconFactory = null; 230 231 ImageIcon getIcon(RosterEntry re) { 232 // defer image handling to RosterIconFactory 233 if (iconFactory == null) { 234 iconFactory = new RosterIconFactory(Math.max(19, new JLabel(getColumnName(0)).getPreferredSize().height)); 235 } 236 return iconFactory.getIcon(re); 237 } 238 239 /** 240 * {@inheritDoc} 241 * 242 * Provides an empty string for a column if the model returns null for that 243 * value. 244 */ 245 @Override 246 public Object getValueAt(int row, int col) { 247 // get roster entry for row 248 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 249 if (re == null) { 250 log.debug("roster entry is null!"); 251 return null; 252 } 253 switch (col) { 254 case IDCOL: 255 return re.getId(); 256 case ADDRESSCOL: 257 return re.getDccLocoAddress().getNumber(); 258 case DECODERMFGCOL: 259 var index = jmri.InstanceManager.getDefault(jmri.jmrit.decoderdefn.DecoderIndexFile.class); 260 var matches = index.matchingDecoderList( 261 null, re.getDecoderFamily(), 262 null, null, null, 263 re.getDecoderModel() 264 ); 265 if (matches.size() == 0) return ""; 266 return matches.get(0).getMfg(); 267 case DECODERFAMILYCOL: 268 return re.getDecoderFamily(); 269 case DECODERMODELCOL: 270 return re.getDecoderModel(); 271 case MODELCOL: 272 return re.getModel(); 273 case ROADNAMECOL: 274 return re.getRoadName(); 275 case ROADNUMBERCOL: 276 return re.getRoadNumber(); 277 case MFGCOL: 278 return re.getMfg(); 279 case ICONCOL: 280 return getIcon(re); 281 case OWNERCOL: 282 return re.getOwner(); 283 case DATEUPDATECOL: 284 // will not display last update if not parsable as date 285 return re.getDateModified(); 286 case PROTOCOL: 287 return re.getProtocolAsString(); 288 default: 289 break; 290 } 291 return getValueAtAttribute(re, col); 292 } 293 294 private Object getValueAtAttribute(RosterEntry re, int col){ 295 String attributeKey = getAttributeKey(col); 296 String value = re.getAttribute(attributeKey); // NOI18N 297 if (RosterEntry.ATTRIBUTE_LAST_OPERATED.equals( attributeKey)) { 298 if (value == null){ 299 return null; 300 } 301 try { 302 return new StdDateFormat().parse(value); 303 } catch (ParseException ex){ 304 return null; 305 } 306 } 307 if ( RosterEntry.ATTRIBUTE_OPERATING_DURATION.equals( attributeKey) ) { 308 try { 309 return Integer.valueOf(value); 310 } 311 catch (NumberFormatException e) { 312 log.debug("could not format duration ( String integer of total seconds ) in {}", value, e); 313 } 314 return 0; 315 } 316 return (value == null ? "" : value); 317 } 318 319 @Override 320 public void setValueAt(Object value, int row, int col) { 321 // get roster entry for row 322 RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row); 323 if (re == null) { 324 log.warn("roster entry is null!"); 325 return; 326 } 327 if (re.isOpen()) { 328 log.warn("Entry is already open"); 329 return; 330 } 331 if (Objects.equals(value, getValueAt(row, col))) { 332 return; 333 } 334 String valueToSet = (String) value; 335 switch (col) { 336 case IDCOL: 337 re.setId(valueToSet); 338 break; 339 case ROADNAMECOL: 340 re.setRoadName(valueToSet); 341 break; 342 case ROADNUMBERCOL: 343 re.setRoadNumber(valueToSet); 344 break; 345 case MFGCOL: 346 re.setMfg(valueToSet); 347 break; 348 case MODELCOL: 349 re.setModel(valueToSet); 350 break; 351 case OWNERCOL: 352 re.setOwner(valueToSet); 353 break; 354 default: 355 setValueAtAttribute(valueToSet, re, col); 356 break; 357 } 358 // need to mark as updated 359 re.changeDateUpdated(); 360 re.updateFile(); 361 } 362 363 private void setValueAtAttribute(String valueToSet, RosterEntry re, int col) { 364 String attributeKey = getAttributeKey(col); 365 if ((valueToSet == null) || valueToSet.isEmpty()) { 366 re.deleteAttribute(attributeKey); 367 } else { 368 re.putAttribute(attributeKey, valueToSet); 369 } 370 } 371 372 public int getPreferredWidth(int column) { 373 int retval = 20; // always take some width 374 retval = Math.max(retval, new JLabel(getColumnName(column)) 375 .getPreferredSize().width + 15); // leave room for sorter arrow 376 for (int row = 0; row < getRowCount(); row++) { 377 if (getColumnClass(column).equals(String.class)) { 378 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 379 } else if (getColumnClass(column).equals(Integer.class)) { 380 retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width); 381 } else if (getColumnClass(column).equals(ImageIcon.class)) { 382 retval = Math.max(retval, new JLabel((Icon) getValueAt(row, column)).getPreferredSize().width); 383 } 384 } 385 return retval + 5; 386 } 387 388 public final void setRosterGroup(String rosterGroup) { 389 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re -> 390 re.removePropertyChangeListener(this)); 391 this.rosterGroup = rosterGroup; 392 Roster.getDefault().getEntriesInGroup(rosterGroup).forEach( re -> 393 re.addPropertyChangeListener(this)); 394 fireTableDataChanged(); 395 } 396 397 public final String getRosterGroup() { 398 return this.rosterGroup; 399 } 400 401 // access via method to ensure not null 402 private String[] attributeKeys = null; 403 404 private String[] getModelAttributeKeyColumnNames() { 405 if ( attributeKeys == null ) { 406 Set<String> result = new TreeSet<>(); 407 for (String s : Roster.getDefault().getAllAttributeKeys()) { 408 if ( !s.contains("RosterGroup") 409 && !s.toLowerCase().startsWith("sys") 410 && !s.toUpperCase().startsWith("VSD")) { // NOI18N 411 result.add(s); 412 } 413 } 414 attributeKeys = result.toArray(String[]::new); 415 } 416 return attributeKeys; 417 } 418 419 private String getAttributeKey(int col) { 420 if ( col >= NUMCOL && col < getColumnCount() ) { 421 return getModelAttributeKeyColumnNames()[col - NUMCOL ]; 422 } 423 return ""; 424 } 425 426 // drop listeners 427 public void dispose() { 428 Roster.getDefault().removePropertyChangeListener(this); 429 Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re -> 430 re.removePropertyChangeListener(this) ); 431 } 432 433 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterTableModel.class); 434 435}