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