Java tutorial
/** * Copyright 2004-2009 Tobias Gierke <tobias.gierke@code-sourcery.de> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.codesourcery.eve.skills.ui.components.impl; import java.awt.Color; import java.awt.Component; import java.awt.GridBagLayout; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.Resource; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.ListSelectionModel; import javax.swing.ToolTipManager; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.log4j.Logger; import de.codesourcery.eve.skills.datamodel.IStaticDataModel; import de.codesourcery.eve.skills.datamodel.PriceInfo; import de.codesourcery.eve.skills.datamodel.PriceInfo.Type; import de.codesourcery.eve.skills.db.dao.IInventoryTypeDAO; import de.codesourcery.eve.skills.db.dao.IRegionDAO; import de.codesourcery.eve.skills.db.dao.IStaticDataModelProvider; import de.codesourcery.eve.skills.db.datamodel.InventoryType; import de.codesourcery.eve.skills.db.datamodel.Region; import de.codesourcery.eve.skills.exceptions.PriceInfoUnavailableException; import de.codesourcery.eve.skills.market.IMarketDataProvider; import de.codesourcery.eve.skills.market.IMarketDataProvider.IPriceInfoChangeListener; import de.codesourcery.eve.skills.market.IMarketDataProvider.IUpdateStrategy; import de.codesourcery.eve.skills.market.IMarketDataProvider.UpdateMode; import de.codesourcery.eve.skills.market.IPriceQueryCallback; import de.codesourcery.eve.skills.market.MarketFilter; import de.codesourcery.eve.skills.market.MarketFilterBuilder; import de.codesourcery.eve.skills.market.MarketLogEntry; import de.codesourcery.eve.skills.market.PriceInfoQueryResult; import de.codesourcery.eve.skills.market.impl.EveMarketLogParser; import de.codesourcery.eve.skills.market.impl.MarketLogFile; import de.codesourcery.eve.skills.market.impl.MarketLogFile.IMarketLogVisitor; import de.codesourcery.eve.skills.ui.components.AbstractComponent; import de.codesourcery.eve.skills.ui.components.ComponentWrapper; import de.codesourcery.eve.skills.ui.components.impl.ByLabelFilterComponent.IItemLabelProvider; import de.codesourcery.eve.skills.ui.config.IAppConfigProvider; import de.codesourcery.eve.skills.ui.config.IRegionQueryCallback; import de.codesourcery.eve.skills.ui.model.IViewFilter; import de.codesourcery.eve.skills.ui.model.ListViewModel; import de.codesourcery.eve.skills.ui.model.impl.PriceInfoTableModel; import de.codesourcery.eve.skills.ui.model.impl.PriceInfoTableModel.TableEntry; import de.codesourcery.eve.skills.ui.utils.GridLayoutBuilder; import de.codesourcery.eve.skills.ui.utils.ImprovedSplitPane; import de.codesourcery.eve.skills.ui.utils.PersistentDialogManager; import de.codesourcery.eve.skills.ui.utils.RegionSelectionDialog; import de.codesourcery.eve.skills.ui.utils.UITask; import de.codesourcery.eve.skills.utils.EveDate; import de.codesourcery.eve.skills.utils.ISystemClock; public class MarketPriceEditorComponent extends AbstractComponent { private static final Logger log = Logger.getLogger(MarketPriceEditorComponent.class); private static final Comparator<? super TableEntry> BY_ITEMNAME_COMPARATOR = new Comparator<TableEntry>() { @Override public int compare(TableEntry o1, TableEntry o2) { return o1.getItem().getName().compareTo(o2.getItem().getName()); } }; private final JTextArea selectedItemDescription = new JTextArea("No item selected."); private final PriceInfoTableModel tableModel; private final MyModel viewModel = new MyModel(); private final JTable table = new JTable(); private final JScrollPane tablePane = new JScrollPane(table); private final JButton importMarketLogButton = new JButton("Import EVE market logfiles"); private final ByLabelFilterComponent<TableEntry> itemNameFilter = new ByLabelFilterComponent<TableEntry>( "Item type filter", new IItemLabelProvider<TableEntry>() { @Override public String getLabelFor(TableEntry obj) { return obj.getItemName(); } @Override public void viewFilterChanged(IViewFilter<TableEntry> filter) { tableModel.viewFilterChanged(); } }); @Resource(name = "dialog-manager") private PersistentDialogManager dialogManager; @Resource(name = "datamodel-provider") private IStaticDataModelProvider dataModelProvider; @Resource(name = "system-clock") private ISystemClock systemClock; @Resource(name = "marketdata-provider") private IMarketDataProvider marketDataProvider; @Resource(name = "appconfig-provider") private IAppConfigProvider appConfigProvider; @Resource(name = "inventory-type-dao") private IInventoryTypeDAO itemDAO; @Resource(name = "region-dao") private IRegionDAO regionDAO; @Resource(name = "static-datamodel") private IStaticDataModel staticDataModel; private int oldTooltipDismissalDelay = -1; private final JPopupMenu popupMenu = new JPopupMenu(); private final class MyModel extends ListViewModel<TableEntry> implements IPriceInfoChangeListener { @Override public void priceChanged(final IMarketDataProvider caller, final Region region, final Set<InventoryType> type) { refreshTable(); } } protected List<PriceInfo> fetchPriceInfo(MarketFilter filter, InventoryType item) { try { final PriceInfoQueryResult result = marketDataProvider.getPriceInfo(filter, null, item); List<PriceInfo> list = new ArrayList<>(); if (result.hasBuyPrice()) { list.add(result.buyPrice()); } if (result.hasSellPrice()) { list.add(result.sellPrice()); } return list; } catch (Exception e) { return Collections.emptyList(); } } /* * Mouse listener that gets attached to the table and displays a * context-sensitive popup-menu. * * Depending on where in the table the user clicked, the menu offers price * update actions for * * - missing buy , sell or buy+sell prices - outdated buy,sell or buy+sell * prices */ private final MouseListener popupListener = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } }; public MarketPriceEditorComponent(IMarketDataProvider marketDataProvider) { super(); this.marketDataProvider = marketDataProvider; this.tableModel = new PriceInfoTableModel(marketDataProvider, getDefaultRegion(), this.viewModel, systemClock); registerChildren(this.itemNameFilter); } /** * Checks whether a mouse-click event should trigger a context-sensitive * popup menu and renders the menu if so. * * @param e */ private void maybeShowPopup(MouseEvent e) { if (!e.isPopupTrigger()) { return; } final int[] selectedRows = table.getSelectedRows(); if (ArrayUtils.isEmpty(selectedRows)) { final int row = table.rowAtPoint(e.getPoint()); if (row < 0) { if (log.isDebugEnabled()) { log.debug("maybeShowPopup(): Outside of table."); } return; } table.getSelectionModel().setSelectionInterval(row, row); } // translate row indices for (int i = 0; i < selectedRows.length; i++) { selectedRows[i] = table.getRowSorter().convertRowIndexToModel(selectedRows[i]); } if (populatePopupMenu(e, selectedRows)) { popupMenu.show(e.getComponent(), e.getX(), e.getY()); } } @Override protected JPanel createPanel() { // add search textfield final JPanel inputPanel = new JPanel(); inputPanel.setBorder(lineBorder(Color.black)); inputPanel.setLayout(new GridBagLayout()); inputPanel.add(itemNameFilter.getPanel(), constraints(0, 0).anchorWest().noResizing().end()); importMarketLogButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { importMarketLogs(); } }); inputPanel.add(importMarketLogButton, constraints(0, 1).anchorWest().noResizing().end()); // add item description text area selectedItemDescription.setEditable(false); selectedItemDescription.setWrapStyleWord(true); selectedItemDescription.setLineWrap(true); final JScrollPane selectedItemDescPanel = new JScrollPane(selectedItemDescription); selectedItemDescPanel.setBorder(BorderFactory.createTitledBorder("Selected item")); // setup table tableModel.setViewFilter(this.itemNameFilter.getViewFilter()); table.setModel(tableModel); table.setRowSorter(tableModel.getRowSorter()); table.setDefaultRenderer(String.class, new StalePriceInfoHighlighter()); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { final int viewRow = table.getSelectedRow(); if (viewRow != -1) { final int modelRow = table.convertRowIndexToModel(viewRow); updateItemDescriptionDisplay(modelRow); } } } }); table.setFillsViewportHeight(true); table.addMouseListener(popupListener); table.setFillsViewportHeight(true); // setup split pane final JPanel result = new JPanel(); final JPanel topPanel = new JPanel(); new GridLayoutBuilder().add(new GridLayoutBuilder.HorizontalGroup(new GridLayoutBuilder.Cell(inputPanel), new GridLayoutBuilder.Cell(selectedItemDescPanel))).addTo(topPanel); final JSplitPane pane = new ImprovedSplitPane(JSplitPane.VERTICAL_SPLIT, topPanel, tablePane); pane.setDividerLocation(0.3d); // only works because of the splitpane // hack pane.setContinuousLayout(true); result.setLayout(new GridBagLayout()); result.add(pane, constraints().resizeBoth().useRemainingSpace().end()); return result; } private void importMarketLogs() { File inputDir = null; if (appConfigProvider.getAppConfig().hasLastMarketLogImportDirectory()) { inputDir = appConfigProvider.getAppConfig().getLastMarketLogImportDirectory(); if (!inputDir.exists() || !inputDir.isDirectory()) { inputDir = null; } } final JFileChooser chooser = inputDir != null ? new JFileChooser(inputDir) : new JFileChooser(); chooser.setFileHidingEnabled(false); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.setMultiSelectionEnabled(true); if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { File[] files = chooser.getSelectedFiles(); if (!ArrayUtils.isEmpty(files)) { appConfigProvider.getAppConfig().setLastMarketLogImportDirectory(files[0].getParentFile()); try { appConfigProvider.save(); } catch (IOException e) { log.error("importMarketLogs(): Failed to save configuration", e); } importMarketLogs(files); } } } private void importMarketLogs(File[] inputFiles) { if (ArrayUtils.isEmpty(inputFiles)) { return; } for (final File current : inputFiles) { final MarketLogFile logFile; try { logFile = new EveMarketLogParser(dataModelProvider.getStaticDataModel(), systemClock) .parseFile(current); } catch (Exception e) { log.error("importMarketLogs(): Failed to parse " + current, e); displayError("Failed to parse logfile " + current.getAbsolutePath() + ": " + e.getMessage()); continue; } // make sure the user is aware when importing data from a file // other than the current default region / // is warned when importing data that conflicts // with already existing data. if (!reallyImportFile(logFile) || logFile.isEmpty()) { log.debug("importMarketLogs(): Skipping file " + current.getAbsoluteFile() + " [ empty / user choice ]"); continue; } final ImportMarketLogFileComponent comp = new ImportMarketLogFileComponent(logFile); comp.setModal(true); final Window window = ComponentWrapper.wrapComponent(current.getName(), comp); if (window == null) { // component may refuse to display // because the file holds data for a region other // from the current default region and the // user chose to cancel the operation continue; } window.setVisible(true); if (comp.wasCancelled()) { continue; // user skipped this file } log.info("importMarketLogs(): Importing data from file " + current.getAbsolutePath()); this.marketDataProvider.store(comp.getPriceInfosForImport()); } } protected boolean reallyImportFile(MarketLogFile logFile) { if (!Region.isSameRegion(logFile.getRegion(), getDefaultRegion())) { final String label = "<HTML><BODY>" + "WARNING !!! <BR><BR>" + "This market logfile holds data from region '" + logFile.getRegion().getName() + "' <BR>while your current " + " default region is set to '" + getDefaultRegion().getName() + "'.</BODY></HTML>"; if (!dialogManager.showTemporaryWarningDialog("marketlog_import_default_region_mismatch", "Region mismatch", label)) { // user pressed the 'cancel' button return false; } } if (!checkForOrdersConflictingWithExistingData(logFile, PriceInfo.Type.BUY)) { return false; } if (!checkForOrdersConflictingWithExistingData(logFile, PriceInfo.Type.SELL)) { return false; } return true; } protected boolean checkForOrdersConflictingWithExistingData(MarketLogFile logFile, final PriceInfo.Type priceType) { // get date of latest price currently known MarketFilter filter = new MarketFilterBuilder(priceType, logFile.getRegion()).end(); final List<PriceInfo> history = fetchPriceInfo(filter, logFile.getInventoryType()); if (history.isEmpty() || logFile.isEmpty()) { return true; } EveDate latestHistoryDate = null; for (PriceInfo info : history) { if (latestHistoryDate == null || info.getTimestamp().compareTo(latestHistoryDate) >= 0) { latestHistoryDate = info.getTimestamp(); } } // get earliest price timestamp from market log final EveDate[] earliestFileDate = new EveDate[1]; logFile.visit(new IMarketLogVisitor() { @Override public void visit(MarketLogEntry entry) { if (!priceType.matches(entry.getType())) { return; } if (earliestFileDate[0] == null || entry.getIssueDate().compareTo(earliestFileDate[0]) < 0) { earliestFileDate[0] = entry.getIssueDate(); } } }); if (earliestFileDate[0] == null) { return true; } // check for overlapping of price data from file // with already known prices if (earliestFileDate[0].compareTo(latestHistoryDate) <= 0) { String typeLabel; switch (priceType) { case BUY: typeLabel = "buy"; break; case SELL: typeLabel = "sell"; break; default: throw new RuntimeException("Unhandled switch/case"); } final String label = "<HTML><BODY>" + "<BR><BR>" + "WARNING: This market logfile contains " + typeLabel + " orders that conflict <BR> with " + "data already present in the application's database.<BR><BR>"; final String[] options = { "Ignore conflicting " + typeLabel + " orders", "Cancel" }; final int userChoice = JOptionPane.showOptionDialog(null, label, "Orders from market log conflict with existing data", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if (userChoice == 0) { final int size = logFile.size(); final int removedCount = logFile.removeEntriesOlderThan(latestHistoryDate, priceType); JOptionPane.showMessageDialog(null, "Removed " + removedCount + " of " + size + " " + typeLabel + " orders total ( left: " + logFile.size() + " )"); } else { return false; } } return true; } private void updateItemDescriptionDisplay(final int index) { final String text; if (index >= 0) { final TableEntry rowData = tableModel.getRow(index); final InventoryType item = rowData.getItem(); final DecimalFormat FORMAT = new DecimalFormat("#########0.000"); String volume = FORMAT.format(item.getVolume()) + " m"; final double numberOfUnitsPerCubicMeter = 1.0d / item.getVolume(); if (numberOfUnitsPerCubicMeter > 1) { volume += " ( " + (int) numberOfUnitsPerCubicMeter + " units / m)"; } text = item.getName() + "\n\n" + item.getDescription() + "\n\nVolume: " + volume + "\nItem ID: " + item.getId(); } else { text = "No item selected."; } selectedItemDescription.setText(text); selectedItemDescription.setCaretPosition(0); } /** * Populates the context-sensitive popup menu by examining the current table * selection. * * @param event * @param selectedRows * @return <code>true</code> if the updated popup menu hast at least one * menu item */ protected boolean populatePopupMenu(MouseEvent event, int[] selectedRows) { if (log.isDebugEnabled()) { log.debug("maybeShowPopup(): selection = " + ObjectUtils.toString(selectedRows)); } popupMenu.removeAll(); final int column = table.columnAtPoint(event.getPoint()); if (column < 0) { log.warn("maybeShowPopup(): Invalid column at point " + event.getPoint()); return false; } log.debug("maybeShowPopup(): column = " + column); // gather all selected rows final List<TableEntry> data = new ArrayList<TableEntry>(); for (int row : selectedRows) { final TableEntry row2 = tableModel.getRow(row); System.out.println(">>> selected row: " + row2.getItemName()); data.add(row2); } populatePopupMenuFor(column, data); return popupMenu.getComponentCount() > 0; } protected void populatePopupMenuFor(int clickedColumn, List<TableEntry> entries) { if (log.isDebugEnabled()) { log.debug("populatePopupMenuFor(): clicked_col = " + clickedColumn + " , selected = " + entries); } boolean gotOutdatedBuyPrice = false; boolean gotOutdatedSellPrice = false; boolean gotUnknownBuyPrices = false; boolean gotUnknownSellPrices = false; boolean gotUserProvidedPrices = false; final Map<Long, InventoryType> updateBuyPrices = new HashMap<Long, InventoryType>(); final Map<Long, InventoryType> updateSellPrices = new HashMap<Long, InventoryType>(); for (TableEntry e : entries) { boolean stale = isStale(e.getBuyPrice()); // method is NULL-safe if (!e.hasBuyPrice() || stale) { if (stale) { if (log.isDebugEnabled()) { log.debug("populatePopupMenuFor(): stale buy price > " + e); } gotOutdatedBuyPrice = true; } if (!e.hasBuyPrice()) { if (log.isDebugEnabled()) { log.debug("populatePopupMenuFor(): Lacking buy price for " + e.getItemName()); } gotUnknownBuyPrices = true; } updateBuyPrices.put(e.getItem().getId(), e.getItem()); } else if (e.getBuyPrice().isUserProvided()) { updateBuyPrices.put(e.getItem().getId(), e.getItem()); gotUserProvidedPrices = true; } stale = isStale(e.getSellPrice()); // method is NULL-safe if (!e.hasSellPrice() || stale) { if (stale) { if (log.isDebugEnabled()) { log.debug("populatePopupMenuFor(): stale sell price > " + e); } gotOutdatedSellPrice = true; } if (!e.hasSellPrice()) { if (log.isDebugEnabled()) { log.debug("populatePopupMenuFor(): Lacking sell price for " + e.getItemName()); } gotUnknownSellPrices = true; } updateSellPrices.put(e.getItem().getId(), e.getItem()); } else if (e.getSellPrice().isUserProvided()) { updateSellPrices.put(e.getItem().getId(), e.getItem()); gotUserProvidedPrices = true; } } if (log.isDebugEnabled()) { log.debug("populatePopupMenuFor(): gotOutdatedBuyPrice=" + gotOutdatedBuyPrice + " , gotOutdatedSellPrice = " + gotOutdatedSellPrice + " , gotUnknownBuyPrices = " + gotUnknownBuyPrices + " , gotUnknownSellPrices = " + gotUnknownSellPrices); } /* * Create and join update request entries. */ final Map<Long, PriceInfoUpdateRequest> requests = new HashMap<Long, PriceInfoUpdateRequest>(); for (InventoryType item : updateBuyPrices.values()) { PriceInfoUpdateRequest existing = requests.get(item.getId()); if (existing == null) { existing = new PriceInfoUpdateRequest(); existing.item = item; existing.type = PriceInfo.Type.BUY; requests.put(item.getId(), existing); } } for (InventoryType item : updateSellPrices.values()) { PriceInfoUpdateRequest existing = requests.get(item.getId()); if (existing == null) { existing = new PriceInfoUpdateRequest(); existing.item = item; existing.type = PriceInfo.Type.SELL; requests.put(item.getId(), existing); } else { if (existing.type == PriceInfo.Type.BUY) { existing.type = PriceInfo.Type.ANY; } } } log.debug("populatePopupMenuFor(): " + " buy prices requiring update: " + updateBuyPrices.size() + " , sell prices requiring update: " + updateSellPrices.size()); final IMenuAction updateOutdatedBuyPrices = new IMenuAction() { @Override public void run() throws PriceInfoUnavailableException { updateMultiplePrices(requests, PriceInfo.Type.BUY, UpdateMode.UPDATE_OUTDATED); } }; final IMenuAction updateMissingBuyPrices = new IMenuAction() { @Override public void run() throws Exception { updateMultiplePrices(requests, PriceInfo.Type.BUY, UpdateMode.UPDATE_MISSING); } }; final IMenuAction updateMissingOrStaleBuyPrices = new IMenuAction() { @Override public void run() throws PriceInfoUnavailableException { updateMultiplePrices(requests, PriceInfo.Type.BUY, UpdateMode.UPDATE_OUTDATED); } }; final IMenuAction updateMissingOrStaleSellPrices = new IMenuAction() { @Override public void run() throws PriceInfoUnavailableException { updateMultiplePrices(requests, PriceInfo.Type.SELL, UpdateMode.UPDATE_MISSING_OR_OUTDATED); } }; final IMenuAction updateOutdatedSellPrices = new IMenuAction() { @Override public void run() throws PriceInfoUnavailableException { updateMultiplePrices(requests, PriceInfo.Type.SELL, UpdateMode.UPDATE_OUTDATED); } }; final IMenuAction updateMissingSellPrices = new IMenuAction() { @Override public void run() throws PriceInfoUnavailableException { updateMultiplePrices(requests, PriceInfo.Type.SELL, UpdateMode.UPDATE_MISSING); } }; final IMenuAction updateUserPricesAction = new IMenuAction() { @Override public void run() throws PriceInfoUnavailableException { updateMultiplePrices(requests, PriceInfo.Type.ANY, UpdateMode.UPDATE_USERPROVIDED); } }; final InventoryType singleItem = entries.size() == 1 ? entries.get(0).getItem() : null; if (!updateBuyPrices.isEmpty()) { String label; if (singleItem != null) { label = "Fetch missing buy price for " + singleItem.getName(); } else { label = "Fetch missing buy prices"; } popupMenu.add(createMenuItem(label, updateMissingBuyPrices, gotUnknownBuyPrices)); if (singleItem != null) { label = "Update outdated buy price for " + singleItem.getName(); } else { label = "Update outdated buy prices"; } popupMenu.add(createMenuItem(label, updateOutdatedBuyPrices, gotOutdatedBuyPrice)); if (singleItem == null) { popupMenu.add(createMenuItem("Update all missing/outdated buy prices", updateMissingOrStaleBuyPrices, gotUnknownBuyPrices | gotOutdatedBuyPrice)); } } if (!updateSellPrices.isEmpty()) { String label; if (singleItem != null) { label = "Fetch missing sell price for " + singleItem.getName(); } else { label = "Fetch missing sell prices"; } popupMenu.add(createMenuItem(label, updateMissingSellPrices, gotUnknownSellPrices)); if (singleItem != null) { label = "Update outdated sell price for " + singleItem.getName(); } else { label = "Update outdated sell prices"; } popupMenu.add(createMenuItem(label, updateOutdatedSellPrices, gotOutdatedSellPrice)); if (singleItem == null) { popupMenu.add(createMenuItem("Update all missing/outdated sell prices", updateMissingOrStaleSellPrices, gotUnknownSellPrices | gotOutdatedSellPrice)); } } if (gotUserProvidedPrices) { if (popupMenu.getComponentCount() > 0) { popupMenu.addSeparator(); } final String label = "Update user-provided prices from EVE-central"; popupMenu.add(createMenuItem(label, updateUserPricesAction, true)); } if (singleItem != null) { if (popupMenu.getComponentCount() > 0) { popupMenu.addSeparator(); } final IMenuAction action = new IMenuAction() { @Override public void run() { final PriceHistoryComponent comp = new PriceHistoryComponent(singleItem, getDefaultRegion(), PriceInfo.Type.ANY); comp.setModal(true); final Window window = ComponentWrapper.wrapComponent("Price history of " + singleItem.getName(), comp); window.setVisible(true); } }; final String label = "Plot price history"; popupMenu.add(createMenuItem(label, action, true)); } } protected static <T> void nullSafeAdd(List<T> list, Collection<T> data) { if (data == null) { return; } list.addAll(data); } protected void updateMultiplePrices(Map<Long, PriceInfoUpdateRequest> requests, PriceInfo.Type type, UpdateMode mode) throws PriceInfoUnavailableException { if (log.isDebugEnabled()) { log.debug("updateMultiplePrices(): " + requests.size() + " items , query type: " + type + " mode = " + mode); } final Map<Type, Set<InventoryType>> pricesByQueryType = new HashMap<Type, Set<InventoryType>>(); for (PriceInfoUpdateRequest request : requests.values()) { Set<InventoryType> existing = pricesByQueryType.get(request.item); if (existing == null) { existing = new HashSet<InventoryType>(); pricesByQueryType.put(type, existing); } existing.add(request.item); } if (log.isDebugEnabled()) { for (Map.Entry<Type, Set<InventoryType>> entry : pricesByQueryType.entrySet()) { log.debug("updateMultiplePrices(): query type " + entry.getKey() + " : " + entry.getValue().size() + " items."); } } final ArrayList<InventoryType> queryBoth = new ArrayList<InventoryType>(); final ArrayList<InventoryType> queryBuy = new ArrayList<InventoryType>(); final ArrayList<InventoryType> querySell = new ArrayList<InventoryType>(); nullSafeAdd(queryBoth, pricesByQueryType.get(PriceInfo.Type.ANY)); nullSafeAdd(queryBuy, pricesByQueryType.get(PriceInfo.Type.BUY)); nullSafeAdd(querySell, pricesByQueryType.get(PriceInfo.Type.SELL)); for (Iterator<InventoryType> it = queryBuy.iterator(); it.hasNext();) { final InventoryType item = it.next(); if (querySell.contains(item)) { it.remove(); querySell.remove(item); queryBoth.add(item); } } for (Iterator<InventoryType> it = querySell.iterator(); it.hasNext();) { final InventoryType item = it.next(); if (queryBuy.contains(item)) { it.remove(); queryBuy.remove(item); queryBoth.add(item); } } if (log.isDebugEnabled()) { log.debug("updateMultiplePrices(): [ after merge ] Type.ALL = " + queryBoth.size()); log.debug("updateMultiplePrices(): [ after merge ] Type.BUY = " + queryBuy.size()); log.debug("updateMultiplePrices(): [ after merge ] Type.SELL = " + querySell.size()); } final IPriceQueryCallback callback = IPriceQueryCallback.NOP_INSTANCE; if (!queryBoth.isEmpty()) { MarketFilter filter = new MarketFilterBuilder(PriceInfo.Type.ANY, getDefaultRegion()).updateMode(mode) .end(); final IUpdateStrategy updateStrategy = marketDataProvider.createUpdateStrategy(mode, PriceInfo.Type.ANY); if (log.isDebugEnabled()) { log.debug("updateMultiplePrices(): Updating Type.ALL item prices."); } marketDataProvider.updatePriceInfo(filter, queryBoth, callback, updateStrategy); } if (!querySell.isEmpty()) { final MarketFilter filter = new MarketFilterBuilder(PriceInfo.Type.SELL, getDefaultRegion()) .updateMode(mode).end(); if (log.isDebugEnabled()) { log.debug("updateMultiplePrices(): Updating Type.SELL item prices."); } final IUpdateStrategy updateStrategy = marketDataProvider.createUpdateStrategy(mode, PriceInfo.Type.SELL); marketDataProvider.updatePriceInfo(filter, querySell, callback, updateStrategy); } if (!queryBuy.isEmpty()) { final MarketFilter filter = new MarketFilterBuilder(PriceInfo.Type.BUY, getDefaultRegion()) .updateMode(mode).end(); if (log.isDebugEnabled()) { log.debug("updateMultiplePrices(): Updating Type.BUY item prices."); } final IUpdateStrategy updateStrategy = marketDataProvider.createUpdateStrategy(mode, PriceInfo.Type.BUY); marketDataProvider.updatePriceInfo(filter, queryBuy, callback, updateStrategy); } } private final class PriceInfoUpdateRequest { public InventoryType item; public PriceInfo.Type type; } private interface IMenuAction { public void run() throws Exception; } protected JMenuItem createMenuItem(String text, final IMenuAction r) { return createMenuItem(text, r, true); } protected JMenuItem createMenuItem(final String text, final IMenuAction r, final boolean enabled) { final JMenuItem item = new JMenuItem(new AbstractAction() { @Override public boolean isEnabled() { return enabled; } @Override public void actionPerformed(ActionEvent e) { submitTask(new UITask() { @Override public String getId() { return text; } @Override public void run() throws Exception { try { r.run(); } catch (PriceInfoUnavailableException e) { displayError("Found no price for " + e.getItemType().getName()); } catch (Exception e1) { displayError("Error: " + e1.getMessage(), e1); } } }); } }); item.setEnabled(enabled); item.setText(text); return item; } protected Region getDefaultRegion() { final IRegionQueryCallback callback = RegionSelectionDialog.createCallback(null, regionDAO); return appConfigProvider.getAppConfig().getDefaultRegion(callback); } protected IStaticDataModel getDataModel() { return this.dataModelProvider.getStaticDataModel(); } protected static PriceInfo findPriceInfo(Type type, List<PriceInfo> infos) { for (PriceInfo info : infos) { if (info.hasType(type)) { return info; } } return null; } private void refreshTable() { final Map<Long, List<PriceInfo>> priceInfos = fetchPrices(); List<TableEntry> priceInfo = new ArrayList<>(); for (Map.Entry<Long, List<PriceInfo>> entry : priceInfos.entrySet()) { final List<PriceInfo> data = entry.getValue(); if (data.size() > 2) { throw new RuntimeException( "Internal error, got " + data.size() + " price infos for item ID " + +entry.getKey()); } final InventoryType item = staticDataModel.getInventoryType(entry.getKey()); final PriceInfo buyPrice = findPriceInfo(Type.BUY, data); final PriceInfo sellPrice = findPriceInfo(Type.SELL, data); if (log.isTraceEnabled()) { log.trace("refreshTable(): [ " + item.getName() + " ] , buy price = " + buyPrice); log.trace("refreshTable(): [ " + item.getName() + " ] , sell price = " + sellPrice); } priceInfo.add(new TableEntry(item, buyPrice, sellPrice)); } Collections.sort(priceInfo, BY_ITEMNAME_COMPARATOR); viewModel.setData(priceInfo); } private Map<Long, List<PriceInfo>> fetchPrices() { final Map<Long, List<PriceInfo>> priceInfos = new HashMap<>(); final Map<Long, InventoryType> knownPrices = this.marketDataProvider .getAllKnownInventoryTypes(getDefaultRegion(), itemDAO); final MarketFilter filter = new MarketFilterBuilder(Type.ANY, getDefaultRegion()).end(); try { final IPriceQueryCallback callback = new IPriceQueryCallback() { @Override public List<PriceInfo> getPriceInfo(MarketFilter filter, String message, InventoryType item) throws PriceInfoUnavailableException { return new ArrayList<>(); } }; final Map<InventoryType, PriceInfoQueryResult> prices = marketDataProvider.getPriceInfos(filter, callback, knownPrices.values().toArray(new InventoryType[knownPrices.size()])); for (Entry<InventoryType, PriceInfoQueryResult> entry : prices.entrySet()) { final List<PriceInfo> list = new ArrayList<>(); PriceInfoQueryResult queryResult = entry.getValue(); if (queryResult.hasBuyPrice()) { list.add(queryResult.buyPrice()); } if (queryResult.hasSellPrice()) { list.add(queryResult.sellPrice()); } priceInfos.put(entry.getKey().getId(), list); } } catch (PriceInfoUnavailableException e) { for (InventoryType t : knownPrices.values()) { priceInfos.put(t.getId(), new ArrayList<PriceInfo>()); } } return priceInfos; } protected final class StalePriceInfoHighlighter extends DefaultTableCellRenderer { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int viewRow, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, viewRow, column); final int modelRow = table.convertRowIndexToModel(viewRow); final TableEntry rowData = tableModel.getRow(modelRow); boolean isStale = false; if (column == PriceInfoTableModel.BUY_PRICE_TIMESTAMP_IDX && rowData.hasBuyPrice() && isStale(rowData.getBuyPrice())) { isStale = true; } if (column == PriceInfoTableModel.SELL_PRICE_TIMESTAMP_IDX && rowData.hasSellPrice() && isStale(rowData.getSellPrice())) { isStale = true; } if (!isSelected) { if ((viewRow % 2) == 0) { setBackground(Color.LIGHT_GRAY); } if (isStale) { setBackground(Color.YELLOW); } else { setBackground(table.getBackground()); } } return this; } } protected boolean isStale(PriceInfo info) { if (info == null) { return false; } return PriceInfoQueryResult.isOutdated(info, systemClock); } @Override protected void disposeHook() { marketDataProvider.removeChangeListener(viewModel); tableModel.dispose(); if (oldTooltipDismissalDelay != -1) { ToolTipManager.sharedInstance().setDismissDelay(oldTooltipDismissalDelay); } } @Override protected void onAttachHook(IComponentCallback callback) { marketDataProvider.addChangeListener(viewModel); oldTooltipDismissalDelay = de.codesourcery.eve.skills.ui.utils.Misc.setTooltipDismissalDelay(8 * 1000); runOnEventThread(new Runnable() { @Override public void run() { refreshTable(); } }); } @Override protected void onDetachHook() { marketDataProvider.removeChangeListener(viewModel); if (oldTooltipDismissalDelay != -1) { ToolTipManager.sharedInstance().setDismissDelay(oldTooltipDismissalDelay); } } }