/* * Copyright 2011 Luke Usherwood. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package net.bettyluke.tracinstant.data; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import javax.swing.table.AbstractTableModel; /** * A bit more that just a "Table" model - this is pretty much the data-model of the whole * app. Perhaps this could be targeted with future refactoring. */ public class TicketTableModel extends AbstractTableModel { // NOTE: "changetime" and "Modified" are aliases, depending on the Trac server version // TODO: Remove "title" at the parsing level? e.g. for memory and search speed? private final Set<String> excludedFields = new TreeSet<>( Arrays.asList("description", "link", "title", "changetime", "Modified")); private static final int TICKET_NUMBER_COLUMN = 0; /** Tickets by row. */ private Ticket[] tickets = new Ticket[0]; /** All fields found in any of the tickets. */ private SortedSet<String> knownFields = new TreeSet<>(); private SortedSet<String> userFields = new TreeSet<>(); /** Columns currently in use. */ private String[] shownColumns = new String[0]; public SortedSet<String> getUserFields() { return Collections.unmodifiableSortedSet(userFields); } public SortedSet<String> getAllFields() { return Collections.unmodifiableSortedSet(knownFields); } public Set<String> getExcludedFields() { return Collections.unmodifiableSet(excludedFields); } public List<Ticket> getTicketsWithAnyField(Collection<String> fields) { List<Ticket> result = new ArrayList<>(tickets.length); for (Ticket t : tickets) { Ticket copy = new Ticket(t.getNumber()); for (String f : fields) { String value = t.getValue(f); if (value != null) { copy.putField(f, value); } } if (!copy.getFieldNames().isEmpty()) { result.add(copy); } } return result; } public void mergeTicketsAsHidden(Collection<Ticket> newTickets) { mergeTicketFieldsInto(newTickets, excludedFields); mergeTickets(newTickets); } public void mergeTickets(Collection<Ticket> newTickets) { if (newTickets.isEmpty()) { return; } int oldRowCount = getRowCount(); // Create a temporary map for sorting and lookup by ID Map<Integer, Ticket> ticketMap = getTicketsAsMap(); for (Ticket newTicket : newTickets) { mergeIntoMap(ticketMap, newTicket); } // Update class members. tickets = ticketMap.values().toArray(new Ticket[0]); mergeTicketFieldsInto(newTickets, knownFields); String[] oldColumns = shownColumns; shownColumns = determineUsedColumns(); if (!Arrays.equals(oldColumns, shownColumns)) { fireTableStructureChanged(); } else { int newRowCount = getRowCount(); if (newRowCount > oldRowCount) { fireTableRowsInserted(oldRowCount, newRowCount - 1); } fireTableRowsUpdated(0, oldRowCount - 1); } } private Map<Integer, Ticket> getTicketsAsMap() { Map<Integer, Ticket> ticketMap = new TreeMap<>(); for (Ticket ticket : tickets) { ticketMap.put(ticket.getNumber(), ticket); } return ticketMap; } private static void mergeIntoMap(Map<Integer, Ticket> ticketMap, Ticket t) { int id = t.getNumber(); Ticket existing = ticketMap.get(id); if (existing == null) { ticketMap.put(id, new Ticket(t)); } else { existing.setFieldsFromTicket(t); } } private void mergeTicketFieldsInto(Collection<Ticket> newTickets, Set<String> set) { for (Ticket ticket : newTickets) { set.addAll(ticket.getFieldNames()); } } private String[] determineUsedColumns() { // TODO: Stub! Make individually selectable. (A custom column model?) Set<String> fields = new TreeSet<>(knownFields); fields.removeAll(excludedFields); fields.add("#"); return fields.toArray(new String[0]); } @Override public int getRowCount() { return tickets.length; } @Override public int getColumnCount() { return shownColumns.length; } @Override public String getColumnName(int column) { return shownColumns[column]; } @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == TICKET_NUMBER_COLUMN) { return Integer.class; } return String.class; } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (columnIndex == TICKET_NUMBER_COLUMN) { return getTicket(rowIndex).getNumber(); } String columnTag = getColumnName(columnIndex); return getTicket(rowIndex).getValue(columnTag); } public Ticket getTicket(int rowIndex) { return tickets[rowIndex]; } /** * @return a snapshot of the tickets. (The array is copied, but Tickets are mutable.) * The tickets are sorted by ticket ID. */ public Ticket[] getTickets() { return Arrays.copyOf(tickets, tickets.length); } /** NB: Slow! */ public Ticket findTicketByID(int ticketId) { for (Ticket t : tickets) { if (t.getNumber() == ticketId) { return t; } } return null; } public void clear() { knownFields.clear(); shownColumns = new String[0]; fireTableStructureChanged(); tickets = new Ticket[0]; fireTableDataChanged(); } public void addUserField(String fieldName) { userFields.add(fieldName); } public void removeUserField(String fieldName) { userFields.remove(fieldName); } }