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 DECODERCOL = 3;
040    static final int ROADNAMECOL = 4;
041    static final int ROADNUMBERCOL = 5;
042    static final int MFGCOL = 6;
043    static final int MODELCOL = 7;
044    static final int OWNERCOL = 8;
045    static final int DATEUPDATECOL = 9;
046    public static final int PROTOCOL = 10;
047    public static final int NUMCOL = PROTOCOL + 1;
048    private String rosterGroup = null;
049    boolean editable = false;
050    
051    public RosterTableModel() {
052        this(false);
053    }
054
055    public RosterTableModel(boolean editable) {
056        this.editable = editable;
057        Roster.getDefault().addPropertyChangeListener(RosterTableModel.this);
058        setRosterGroup(null); // add prop change listeners to roster entries
059    }
060
061    /**
062     * Create a table model for a Roster group.
063     *
064     * @param group the roster group to show; if null, behaves the same as
065     *              {@link #RosterTableModel()}
066     */
067    public RosterTableModel(@CheckForNull RosterGroup group) {
068        this(false);
069        if (group != null) {
070            this.setRosterGroup(group.getName());
071        }
072    }
073
074    @Override
075    public void propertyChange(PropertyChangeEvent e) {
076        if (e.getPropertyName().equals(Roster.ADD)) {
077            setRosterGroup(getRosterGroup()); // add prop change listener to new entry
078            fireTableDataChanged();
079        } else if (e.getPropertyName().equals(Roster.REMOVE)) {
080            fireTableDataChanged();
081        } else if (e.getPropertyName().equals(Roster.SAVED)) {
082            //TODO This really needs to do something like find the index of the roster entry here
083            if (e.getSource() instanceof RosterEntry) {
084                int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource());
085                fireTableRowsUpdated(row, row);
086            } else {
087                fireTableDataChanged();
088            }
089        } else if (e.getPropertyName().equals(RosterGroupSelector.SELECTED_ROSTER_GROUP)) {
090            setRosterGroup((e.getNewValue() != null) ? e.getNewValue().toString() : null);
091        } else if (e.getPropertyName().startsWith("attribute") && e.getSource() instanceof RosterEntry) { // NOI18N
092            int row = Roster.getDefault().getGroupIndex(rosterGroup, (RosterEntry) e.getSource());
093            fireTableRowsUpdated(row, row);
094        } else if (e.getPropertyName().equals(Roster.ROSTER_GROUP_ADDED) && e.getNewValue().equals(rosterGroup)) {
095            fireTableDataChanged();
096        }
097    }
098
099    @Override
100    public int getRowCount() {
101        return Roster.getDefault().numGroupEntries(rosterGroup);
102    }
103
104    @Override
105    public int getColumnCount() {
106        return NUMCOL + getModelAttributeKeyColumnNames().length;
107    }
108
109    @Override
110    public String getColumnName(int col) {
111        switch (col) {
112            case IDCOL:
113                return Bundle.getMessage("FieldID");
114            case ADDRESSCOL:
115                return Bundle.getMessage("FieldDCCAddress");
116            case DECODERCOL:
117                return Bundle.getMessage("FieldDecoderModel");
118            case MODELCOL:
119                return Bundle.getMessage("FieldModel");
120            case ROADNAMECOL:
121                return Bundle.getMessage("FieldRoadName");
122            case ROADNUMBERCOL:
123                return Bundle.getMessage("FieldRoadNumber");
124            case MFGCOL:
125                return Bundle.getMessage("FieldManufacturer");
126            case ICONCOL:
127                return Bundle.getMessage("FieldIcon");
128            case OWNERCOL:
129                return Bundle.getMessage("FieldOwner");
130            case DATEUPDATECOL:
131                return Bundle.getMessage("FieldDateUpdated");
132            case PROTOCOL:
133                return Bundle.getMessage("FieldProtocol");
134            default:
135                return getColumnNameAttribute(col);
136        }
137    }
138
139    private String getColumnNameAttribute(int col) {
140        if ( col < getColumnCount() ) {
141            String attributeKey = getAttributeKey(col);
142            try {
143                return Bundle.getMessage(attributeKey);
144            } catch (java.util.MissingResourceException ex){}
145
146            String[] r = attributeKey.split("(?=\\p{Lu})"); // NOI18N
147            StringBuilder sb = new StringBuilder();
148            sb.append(r[0].trim());
149            for (int j = 1; j < r.length; j++) {
150                sb.append(" ");
151                sb.append(r[j].trim());
152            }
153            return sb.toString();
154        }
155        return "<UNKNOWN>"; // NOI18N
156    }
157
158    @Override
159    public Class<?> getColumnClass(int col) {
160        switch (col) {
161            case ADDRESSCOL:
162                return Integer.class;
163            case ICONCOL:
164                return ImageIcon.class;
165            case DATEUPDATECOL:
166                return Date.class;
167            default:
168                return getColumnClassAttribute(col);
169        }
170    }
171
172    private Class<?> getColumnClassAttribute(int col){
173        if (RosterEntry.ATTRIBUTE_LAST_OPERATED.equals( getAttributeKey(col))) {
174            return Date.class;
175        }
176        if (RosterEntry.ATTRIBUTE_OPERATING_DURATION.equals( getAttributeKey(col))) {
177            return Integer.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        if ( RosterEntry.ATTRIBUTE_OPERATING_DURATION.equals( attributeKey) ) {
285            try {
286                return Integer.valueOf(value);
287            }
288            catch (NumberFormatException e) {
289                log.debug("could not format duration ( String integer of total seconds ) in {}", value, e);
290            }
291            return 0;
292        }
293        return (value == null ? "" : value);
294    }
295
296    @Override
297    public void setValueAt(Object value, int row, int col) {
298        // get roster entry for row
299        RosterEntry re = Roster.getDefault().getGroupEntry(rosterGroup, row);
300        if (re == null) {
301            log.warn("roster entry is null!");
302            return;
303        }
304        if (re.isOpen()) {
305            log.warn("Entry is already open");
306            return;
307        }
308        if (Objects.equals(value, getValueAt(row, col))) {
309            return;
310        }
311        String valueToSet = (String) value;
312        switch (col) {
313            case IDCOL:
314                re.setId(valueToSet);
315                break;
316            case ROADNAMECOL:
317                re.setRoadName(valueToSet);
318                break;
319            case ROADNUMBERCOL:
320                re.setRoadNumber(valueToSet);
321                break;
322            case MFGCOL:
323                re.setMfg(valueToSet);
324                break;
325            case MODELCOL:
326                re.setModel(valueToSet);
327                break;
328            case OWNERCOL:
329                re.setOwner(valueToSet);
330                break;
331            default:
332                setValueAtAttribute(valueToSet, re, col);
333                break;
334        }
335        // need to mark as updated
336        re.changeDateUpdated();
337        re.updateFile();
338    }
339
340    private void setValueAtAttribute(String valueToSet, RosterEntry re, int col) {
341        String attributeKey = getAttributeKey(col);
342        if ((valueToSet == null) || valueToSet.isEmpty()) {
343            re.deleteAttribute(attributeKey);
344        } else {
345            re.putAttribute(attributeKey, valueToSet);
346        }
347    }
348
349    public int getPreferredWidth(int column) {
350        int retval = 20; // always take some width
351        retval = Math.max(retval, new JLabel(getColumnName(column))
352            .getPreferredSize().width + 15);  // leave room for sorter arrow
353        for (int row = 0; row < getRowCount(); row++) {
354            if (getColumnClass(column).equals(String.class)) {
355                retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width);
356            } else if (getColumnClass(column).equals(Integer.class)) {
357                retval = Math.max(retval, new JLabel(getValueAt(row, column).toString()).getPreferredSize().width);
358            } else if (getColumnClass(column).equals(ImageIcon.class)) {
359                retval = Math.max(retval, new JLabel((Icon) getValueAt(row, column)).getPreferredSize().width);
360            }
361        }
362        return retval + 5;
363    }
364
365    public final void setRosterGroup(String rosterGroup) {
366        Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re ->
367            re.removePropertyChangeListener(this));
368        this.rosterGroup = rosterGroup;
369        Roster.getDefault().getEntriesInGroup(rosterGroup).forEach( re ->
370            re.addPropertyChangeListener(this));
371        fireTableDataChanged();
372    }
373
374    public final String getRosterGroup() {
375        return this.rosterGroup;
376    }
377
378    // access via method to ensure not null
379    private String[] attributeKeys = null; 
380
381    private String[] getModelAttributeKeyColumnNames() {
382        if ( attributeKeys == null ) {
383            Set<String> result = new TreeSet<>();
384            for (String s : Roster.getDefault().getAllAttributeKeys()) {
385                if ( !s.contains("RosterGroup")
386                    && !s.toLowerCase().startsWith("sys")
387                    && !s.toUpperCase().startsWith("VSD")) { // NOI18N
388                    result.add(s);
389                }
390            }
391            attributeKeys = result.toArray(String[]::new);
392            }
393        return attributeKeys;
394    }
395
396    private String getAttributeKey(int col) {
397        if ( col >= NUMCOL && col < getColumnCount() ) {
398            return getModelAttributeKeyColumnNames()[col - NUMCOL ];
399        }
400        return "";
401    }
402
403    // drop listeners
404    public void dispose() {
405        Roster.getDefault().removePropertyChangeListener(this);
406        Roster.getDefault().getEntriesInGroup(this.rosterGroup).forEach( re ->
407            re.removePropertyChangeListener(this) );
408    }
409
410    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterTableModel.class);
411
412}