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