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}