package org.rr.jeborker.gui.model; import static org.rr.commons.utils.StringUtil.EMPTY; import java.lang.reflect.Field; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import javax.swing.SwingUtilities; import javax.swing.event.EventListenerList; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import org.rr.commons.collection.BlindElementList; import org.rr.commons.collection.CompoundList; import org.rr.commons.collection.ICloseableList; import org.rr.commons.collection.InsertElementList; import org.rr.commons.collection.ReplacementElementList; import org.rr.commons.log.LoggerFactory; import org.rr.jeborker.app.FileRefreshBackground; import org.rr.jeborker.db.DBUtils; import org.rr.jeborker.db.DefaultDBManager; import org.rr.jeborker.db.OrderDirection; import org.rr.jeborker.db.item.EbookPropertyItem; import org.rr.jeborker.db.item.EbookPropertyItemUtils; import com.j256.ormlite.stmt.NullArgHolder; import com.j256.ormlite.stmt.Where; public class EbookPropertyDBTableModel extends AbstractEbookPropertyTableModel { public abstract static class EbookPropertyDBTableModelQuery { public boolean isVolatile() { return false; } public void appendQuery(Where<EbookPropertyItem, EbookPropertyItem> where) throws SQLException { where.raw("true = true", new NullArgHolder[0]); }; public void appendKeyword(List<String> keyword) {}; public abstract String getIdentifier(); @Override public boolean equals(Object obj) { if(this == obj) { return true; } else if(obj instanceof EbookPropertyDBTableModelQuery) { return ((EbookPropertyDBTableModelQuery)obj).getIdentifier().equals(this.getIdentifier()); } return false; } }; /** List of listeners */ private EventListenerList listenerList = new EventListenerList(); private ICloseableList<EbookPropertyItem> dbItems; private List<EbookPropertyItem> allItems; private final List<EbookPropertyDBTableModelQuery> whereConditions = new ArrayList<>(); private final List<Field> orderByColumns = new ArrayList<>(); private OrderDirection orderDirection = null; private boolean dirty = false; private int oldSize = 0; private boolean emptyModel = false; public EbookPropertyDBTableModel(boolean emptyModel) { super(); this.emptyModel = emptyModel; } public EbookPropertyDBTableModel(EbookPropertyDBTableModel copy, boolean emptyModel) { super(); this.emptyModel = emptyModel; setOrderByColumns(copy.getOrderByColumns()); setOrderDirection(copy.getOrderDirection()); whereConditions.addAll(copy.whereConditions); } /** * Adds a listener to the list that's notified each time a change to the data model occurs. * * @param l the TableModelListener */ @Override public void addTableModelListener(TableModelListener l) { listenerList.add(TableModelListener.class, l); } @Override public Class<?> getColumnClass(int columnIndex) { return String.class; } @Override public int getColumnCount() { return 1; } @Override public String getColumnName(int columnIndex) { return Bundle.getString("EbookPropertyDBTableModel.headline"); } @Override public int getRowCount() { final List<EbookPropertyItem> ebookItems = this.getEbookItems(); if(ebookItems != null) { final int size = ebookItems.size(); if(size > oldSize) { final int old = oldSize; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { fireTableRowsInserted(old-1, size-1); } }); } this.oldSize = size; return size; } else { return 0; } } @Override public Object getValueAt(int rowIndex, int columnIndex) { List<EbookPropertyItem> ebookItems = this.getEbookItems(); final EbookPropertyItem ebookPropertyItem; try { if(ebookItems.size() < rowIndex) { setDirty(); ebookItems = this.getEbookItems(); } ebookPropertyItem = ebookItems.get(rowIndex); } catch(IndexOutOfBoundsException ex) { return null; } if(ebookPropertyItem != null) { FileRefreshBackground.getInstance().addEbook(ebookPropertyItem); switch(columnIndex) { case 0: return ebookPropertyItem; } } return null; } /** * Gets the {@link EbookPropertyItem} displayed in the given row. * @return The desired {@link EbookPropertyItem} or possibly <code>null</code>. */ public EbookPropertyItem getEbookPropertyItemAt(int rowIndex) { final List<EbookPropertyItem> ebookItems = this.getEbookItems(); try { return ebookItems.get(rowIndex); } catch(IndexOutOfBoundsException ex) { LoggerFactory.logInfo(this, EMPTY, ex); return null; } } /** * Loads the {@link EbookPropertyItem} element at the given index from the database * and set this instance as current one. * @param rowIndex Index of {@link EbookPropertyItem} to be reloaded. */ public void reloadEbookPropertyItemAt(int rowIndex) { final List<EbookPropertyItem> ebookItems = this.getEbookItems(); if(rowIndex >= 0) { final EbookPropertyItem modelItem = this.getEbookPropertyItemAt(rowIndex); if(modelItem != null) { EbookPropertyItem item = EbookPropertyItemUtils.reloadEbookPropertyItem(modelItem); if(item != null) { EbookPropertyItem dbItem = item; if(dbItem.getFile().equals(modelItem.getFile())) { this.allItems = new ReplacementElementList<>(ebookItems, rowIndex, dbItem); } } } } } public int searchRow(EbookPropertyItem item) { if(item == null) { return -1; } final List<EbookPropertyItem> ebookItems = this.getEbookItems(); for(int i = 0; i < ebookItems.size(); i++) { EbookPropertyItem ebookPropertyItem = ebookItems.get(i); if(ebookPropertyItem.equals(item)) { return i; } } return -1; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if(columnIndex == 0) { return true; } return false; } /** * Removes a listener from the list that's notified each time a change to the data model occurs. * * @param l the TableModelListener */ public void removeTableModelListener(TableModelListener l) { listenerList.remove(TableModelListener.class, l); } @Override public void setValueAt(Object aValue, int row, int column) { fireTableCellUpdated(row, column); } /** * Notifies all listeners that the value of the cell at <code>[row, column]</code> has been updated. * * @param row row of cell which has been updated * @param column column of cell which has been updated * @see TableModelEvent * @see EventListenerList */ public void fireTableCellUpdated(int row, int column) { fireTableChanged(new TableModelEvent(this, row, row, column)); } /** * Forwards the given notification event to all <code>TableModelListeners</code> that registered themselves as listeners for this table * model. * * @param e the event to be forwarded * * @see #addTableModelListener * @see TableModelEvent * @see EventListenerList */ public void fireTableChanged(TableModelEvent e) { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == TableModelListener.class) { ((TableModelListener) listeners[i + 1]).tableChanged(e); } } } /** * Notifies all listeners that rows in the range <code>[firstRow, lastRow]</code>, inclusive, have been inserted. * * @param firstRow the first row * @param lastRow the last row * * @see TableModelEvent * @see EventListenerList */ public void fireTableRowsInserted(int firstRow, int lastRow) { fireTableChanged(new TableModelEvent(this, firstRow, lastRow + 1, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT)); } /** * Notifies all listeners that rows in the range <code>[firstRow, lastRow]</code>, inclusive, have been deleted. * * @param firstRow the first row * @param lastRow the last row * * @see TableModelEvent * @see EventListenerList */ public void fireTableRowsDeleted(int firstRow, int lastRow) { fireTableChanged(new TableModelEvent(this, firstRow, lastRow, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE)); } /** * Get the {@link EbookPropertyItem}s to be handled by this model. * * @param refresh <code>true</code> if the data should be refreshed and <code>false</code> for using them from cache. * @return The desired {@link EbookPropertyItem}s. */ private List<EbookPropertyItem> getEbookItems() { // NO BREAKPOINT HERE! if (emptyModel) { this.allItems = Collections.emptyList(); } else if (isDirty() || dbItems == null) { this.dirty = false; Where<EbookPropertyItem, EbookPropertyItem> whereConditions = prepareQuery(); List<String> keywords = prepareKeywords(); ICloseableList<EbookPropertyItem> items = DefaultDBManager.getInstance().queryFullTextSearch(EbookPropertyItem.class, whereConditions, keywords, getOrderByColumns(), getOrderDirection()); clearVolatileConditions(); if (this.dbItems != null) { this.dbItems.close(); } this.dbItems = items; List<EbookPropertyItem> addedItems = new ArrayList<>(); this.allItems = new CompoundList<>(dbItems, addedItems); } return this.allItems; } private List<String> prepareKeywords() { ArrayList<String> result = new ArrayList<>(); for (EbookPropertyDBTableModelQuery whereCondition : whereConditions) { whereCondition.appendKeyword(result); } return result; } private Where<EbookPropertyItem, EbookPropertyItem> prepareQuery() { List<EbookPropertyDBTableModelQuery> toRemove = new ArrayList<>(); Where<EbookPropertyItem, EbookPropertyItem> where = DefaultDBManager.getInstance().getQueryBuilder(EbookPropertyItem.class).where(); for (EbookPropertyDBTableModelQuery whereCondition : whereConditions) { try { if (!DBUtils.isEmpty(where)) { where.and(); } whereCondition.appendQuery(where); } catch (SQLException e) { LoggerFactory.log(Level.SEVERE, this, "Failed to prepare Query", e); } if (whereCondition.isVolatile()) { toRemove.add(whereCondition); } } whereConditions.removeAll(toRemove); return where; } /** * Attaches an {@link EbookPropertyItem} to the specified row. If the row parameter is -1 the value is added to the end of the list. * * @param item The item to be attached. * @param row The row where the {@link EbookPropertyItem} should be added to. */ public void addRow(EbookPropertyItem item, int row) { if (row < 0) { boolean isInsert = false; try { isInsert = this.allItems.add(item); } catch (java.lang.UnsupportedOperationException e) { // not allowed to add this.allItems = new CompoundList<EbookPropertyItem>(this.allItems, new ArrayList<>(Arrays.asList(item))); isInsert = true; } if (isInsert) { final int ins = this.allItems.size() - 1; fireTableRowsInserted(ins, ins); } } else { this.allItems = new InsertElementList<>(this.allItems, item, row); fireTableRowsInserted(row, row); } } public boolean removeRow(final EbookPropertyItem item) { final Iterator<EbookPropertyItem> iterator = this.allItems.iterator(); boolean removed = false; for (int i = 0; iterator.hasNext(); i++) { final EbookPropertyItem toRemove = iterator.next(); try { if (toRemove != null && toRemove.equals(item)) { DefaultDBManager.getInstance().deleteObject(toRemove); if (!isDirty()) { this.allItems = new BlindElementList<>(this.allItems, i); fireTableRowsDeleted(i, i); } removed = true; break; } } catch (Exception e) { this.setDirty(); break; } } return removed; } /** * @return <code>true</code> if a refresh is needed and <code>false</code> otherwise. * @see #setDirty() */ public boolean isDirty() { return dirty; } /** * Tells the model that the data has changed and next time the data should be refetched from the database. * * @see #isDirty() */ public void setDirty() { this.dirty = true; } public List<Field> getOrderByColumns() { return orderByColumns; } /** * Set the order by columns to the table model. To apply the new order invoke the {@link #setDirty()} method or directly do a * <code>MainController.getController().refreshTable(true);</code> * * @param orderByColumns Order columns to be set to the model. Previously set order columns will be removed. */ public void setOrderByColumns(List<Field> orderByColumns) { this.orderByColumns.clear(); this.orderByColumns.addAll(orderByColumns); } /** * Gets the current order direction. Ascending order is the default one. * * @return The current order direction. */ public OrderDirection getOrderDirection() { if (this.orderDirection == null) { this.orderDirection = new OrderDirection(OrderDirection.DIRECTION_ASC); } return this.orderDirection; } /** * Sets the order direction for the displayed data. * * @param orderDirection The order direction to be used for the data. */ public void setOrderDirection(final OrderDirection orderDirection) { this.orderDirection = orderDirection; } /** * Remove all conditions marked as volatile conditions. */ private void clearVolatileConditions() { List<EbookPropertyDBTableModelQuery> toRemove = new ArrayList<EbookPropertyDBTableModel.EbookPropertyDBTableModelQuery>(); for (EbookPropertyDBTableModelQuery whereCondition : whereConditions) { if (whereCondition.isVolatile()) { toRemove.add(whereCondition); } } whereConditions.removeAll(toRemove); } /** * Adds a {@link EbookPropertyDBTableModelQuery} which will be used to create the model's sql query. */ public void addWhereCondition(EbookPropertyDBTableModelQuery query) { removeWhereCondition(query.getIdentifier()); whereConditions.add(query); } /** * Removes a {@link EbookPropertyDBTableModelQuery} which was previously added with the * {@link #addWhereCondition(EbookPropertyDBTableModelQuery)} method. * * @param identifier The identifier of the {@link EbookPropertyDBTableModelQuery} instance to be removed. the * {@link EbookPropertyDBTableModelQuery#getIdentifier()} method is used to identify the {@link EbookPropertyDBTableModelQuery} * which gets removed. * @return <code>true</code> if a condition was removed and <code>false</code> otherwise. */ public boolean removeWhereCondition(String identifier) { List<EbookPropertyDBTableModelQuery> toRemove = new ArrayList<EbookPropertyDBTableModel.EbookPropertyDBTableModelQuery>(); for (EbookPropertyDBTableModelQuery whereCondition : whereConditions) { if (whereCondition.getIdentifier().compareTo(identifier) == 0) { toRemove.add(whereCondition); } } return whereConditions.removeAll(toRemove); } }