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.market.impl; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import javax.swing.SwingUtilities; import javax.xml.xpath.XPathExpression; import org.apache.commons.lang.ArrayUtils; import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; import org.apache.log4j.Logger; import org.springframework.beans.factory.DisposableBean; import org.w3c.dom.Document; import org.w3c.dom.Element; import de.codesourcery.eve.apiclient.utils.XMLParseHelper; 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.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.IPriceInfoStore; import de.codesourcery.eve.skills.market.IPriceQueryCallback; import de.codesourcery.eve.skills.market.MarketFilter; import de.codesourcery.eve.skills.market.PriceInfoQueryResult; import de.codesourcery.eve.skills.util.IStatusCallback; import de.codesourcery.eve.skills.util.Misc; import de.codesourcery.eve.skills.utils.EveDate; import de.codesourcery.eve.skills.utils.ISystemClock; public class EveCentralMarketDataProvider extends XMLParseHelper implements IMarketDataProvider, DisposableBean { public static final Logger LOG = Logger.getLogger(EveCentralMarketDataProvider.class); /** * Time to wait before retrying * to obtain a missing price from eve central. */ public static final long RETRY_DELAY_ON_MISSING_PRICES = 2 * 60 * 60 * 1000; // 2 hours private ISystemClock systemClock; private final List<IPriceInfoChangeListener> listeners = new ArrayList<IPriceInfoChangeListener>(); private IEveCentralClient eveCentralClient; private volatile boolean isOfflineMode; private IPriceInfoStore priceInfoStore; /** * key = market filter * value = Map<Inventory time ID,Unix timestamp of last attempt to fetch price from eve central> */ private final Map<MarketFilter, Map<Long, Long>> unknownPrices = new HashMap<MarketFilter, Map<Long, Long>>(); protected final class UPDATE_NONE_STRATEGY implements IUpdateStrategy { @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { return false; } @Override public void merge(MarketFilter filter, PriceInfoQueryResult result, PriceInfo existing) { } @Override public String toString() { return "Update nothing."; } }; private abstract class AbstractUpdateStrategy implements IUpdateStrategy { private final PriceInfo.Type filterType; protected AbstractUpdateStrategy(PriceInfo.Type filterType) { if (filterType == null) { throw new IllegalArgumentException("filterType cannot be NULL"); } this.filterType = filterType; } protected PriceInfo.Type getFilterType() { return filterType; } @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { if (existingInfo == null) { return true; } return filterType.matches(existingInfo.getPriceType()); } @Override public final void merge(MarketFilter filter, PriceInfoQueryResult result, PriceInfo existing) { result.merge(filter.getOrderType(), existing); } } protected final class UpdateMissingOrOutdatedPrices extends AbstractUpdateStrategy { protected UpdateMissingOrOutdatedPrices(Type filterType) { super(filterType); } @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { if (!super.requiresUpdate(item, existingInfo)) { return false; } // only try to update data that was // provided by the user. return existingInfo == null || isOutdated(existingInfo); } @Override public String toString() { return "Missing or outdated and not user-provided"; } }; protected final class UpdateUserProvidedPrices extends AbstractUpdateStrategy { protected UpdateUserProvidedPrices(Type filterType) { super(filterType); } @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { if (!super.requiresUpdate(item, existingInfo)) { return false; } // only try to update data that was // provided by the user. return existingInfo != null && existingInfo.isUserProvided(); } @Override public String toString() { return "Missing or outdated and not user-provided"; } }; protected final class DEFAULT_STRATEGY extends AbstractUpdateStrategy { protected DEFAULT_STRATEGY(Type filterType) { super(filterType); } @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { if (!super.requiresUpdate(item, existingInfo)) { return false; } // only try to update data that is either missing // or not provided by the user and outdated if (LOG.isDebugEnabled()) { if (existingInfo == null) { LOG.debug("requiresUpdate(): No price available for " + item); } else if (!existingInfo.isUserProvided() && isOutdated(existingInfo)) { LOG.debug("requiresUpdate(): Price for " + item + " is outdated and not user-provided"); } } return existingInfo == null || (!existingInfo.isUserProvided() && isOutdated(existingInfo)); } @Override public String toString() { return "Missing or outdated and not user-provided"; } }; protected final class UPDATE_ALL_STRATEGY extends AbstractUpdateStrategy { protected UPDATE_ALL_STRATEGY(Type filterType) { super(filterType); } @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { return true; } @Override public String toString() { return "Update all kinds of prices"; } }; protected final class UPDATE_MISSING_STRATEGY extends AbstractUpdateStrategy { protected UPDATE_MISSING_STRATEGY(Type filterType) { super(filterType); } @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { if (!super.requiresUpdate(item, existingInfo)) { return false; } return existingInfo == null; } @Override public String toString() { return "Update only missing prices"; } }; private final class UPDATE_OUTDATED_STRATEGY extends AbstractUpdateStrategy { protected UPDATE_OUTDATED_STRATEGY(Type filterType) { super(filterType); } @Override public boolean requiresUpdate(InventoryType item, PriceInfo existingInfo) { if (!super.requiresUpdate(item, existingInfo)) { return false; } return existingInfo != null && isOutdated(existingInfo); } @Override public String toString() { return "Update only outdated prices"; } }; public EveCentralMarketDataProvider() { } public void setSystemClock(ISystemClock systemClock) { if (systemClock == null) { throw new IllegalArgumentException("systemClock cannot be NULL"); } this.systemClock = systemClock; } @Override public PriceInfoQueryResult getPriceInfo(MarketFilter filter, IPriceQueryCallback callback, InventoryType type) throws PriceInfoUnavailableException { final PriceInfoQueryResult result = getPriceInfos(filter, callback, type).get(type); if (result == null) { throw new PriceInfoUnavailableException("No price info", type); // return new PriceInfoQueryResult( type ); } return result; } /* <!-- This is the new API :-) --> <evec_api version="2.0" method="marketstat_xml"> <marketstat> <type id="34"> <all> <volume>41337268582</volume> <avg>36.8872779152</avg> <max>11155.08</max> <min>0.3</min> <stddev>552.601683331</stddev> <median>4.15</median> </all> <buy> <volume>23271663605</volume> <avg>3.96983095868</avg> <max>4.3</max> <min>0.3</min> <stddev>0.572456683074</stddev> <median>3.87</median> </buy> <sell> <volume>18065604977</volume> <avg>4.40662120896</avg> <max>11155.08</max> <min>3.9</min> <stddev>640.33788886</stddev> <median>4.18</median> </sell> </type> */ protected PriceInfoQueryResult getCachedEntry(MarketFilter filter, InventoryType type) { final List<PriceInfo> data = priceInfoStore.get(filter, type); return new PriceInfoQueryResult(type, data); } protected void storeCacheEntry(PriceInfo entry) { if (LOG.isDebugEnabled()) { LOG.debug("storeCacheEntry(): entry = " + entry); } priceInfoStore.save(entry); } protected boolean isPriceMissingOnEveCentral(MarketFilter filter, InventoryType type) { final Map<Long, Long> missingItemsById = unknownPrices.get(filter); if (missingItemsById == null) { return false; } if (missingItemsById.get(type.getId()) != null) { return true; } return false; } protected void forgetPriceMissingOnEveCentral(MarketFilter filter, InventoryType type) { final Map<Long, Long> missingItemsById = unknownPrices.get(filter); if (missingItemsById == null) { return; } missingItemsById.remove(type.getId()); } protected void rememberPriceMissingOnEveCentral(MarketFilter filter, InventoryType type) { if (LOG.isDebugEnabled()) { LOG.debug("rememberPriceMissingOnEveCentral(): item " + type + " , filter " + filter); } Map<Long, Long> missingItemsById = unknownPrices.get(filter); if (missingItemsById == null) { missingItemsById = new HashMap<Long, Long>(); unknownPrices.put(filter, missingItemsById); } missingItemsById.put(type.getId(), System.currentTimeMillis()); } protected boolean mayQueryAgainForMissingPrice(MarketFilter filter, InventoryType type) { final Map<Long, Long> missingItemsById = unknownPrices.get(filter); if (missingItemsById == null) { return true; } final Long timestamp = missingItemsById.get(type.getId()); if (timestamp == null) { return true; } final long delta = System.currentTimeMillis() - timestamp; return delta >= RETRY_DELAY_ON_MISSING_PRICES; } private void runOnEDT(Runnable r) { Misc.runOnEventThread(r); } @Override public Map<InventoryType, PriceInfoQueryResult> getPriceInfos(final MarketFilter filter, final IPriceQueryCallback callback, final InventoryType... items) throws PriceInfoUnavailableException { if (LOG.isDebugEnabled()) { LOG.debug("getPriceInfos(): filter = " + filter + ", items = " + items); } if (ArrayUtils.isEmpty(items)) { return Collections.emptyMap(); } final AtomicReference<Map<InventoryType, PriceInfoQueryResult>> resultHolder = new AtomicReference<Map<InventoryType, PriceInfoQueryResult>>( new ConcurrentHashMap<InventoryType, PriceInfoQueryResult>()); final IUpdateStrategy updateStrategy = createUpdateStrategy(filter.getUpdateMode(), filter.getOrderType()); final Vector<NameValuePair> params = new Vector<NameValuePair>(); /* * NEEDS to be run on the EDT since Hibernate * lazy-fetching might kick in and * the Hibernate session is confined to the EDT. */ runOnEDT(new Runnable() { @Override public void run() { if (LOG.isDebugEnabled()) { LOG.debug("getPriceInfos(): update_strategy = " + updateStrategy); } for (InventoryType t : items) { // make sure we don't query over and over // for prices that are unavailable anyway if (isPriceMissingOnEveCentral(filter, t)) { if (!mayQueryAgainForMissingPrice(filter, t)) { if (LOG.isDebugEnabled()) { LOG.debug("getPriceInfos(): " + "Price for " + t + " " + "unavailable on eve-central , filter " + filter); } continue; } if (LOG.isDebugEnabled()) { LOG.debug("getPriceInfos(): [ retrying ] " + "Price for " + t + " " + "unavailable on eve-central , filter " + filter); } } final PriceInfoQueryResult cached = getCachedEntry(filter, t); resultHolder.get().put(t, cached); if (LOG.isDebugEnabled()) { if (cached.isEmpty()) { LOG.debug("getPriceInfos(): [ NOT CACHED ] type = " + t.getId() + " , name = " + t.getName()); } else { LOG.debug("getPriceInfos(): [ CACHE HIT ] " + cached); } } final boolean requiresUpdate; switch (filter.getOrderType()) { case BUY: requiresUpdate = updateStrategy.requiresUpdate(t, cached.hasBuyPrice() ? cached.buyPrice() : null); break; case SELL: requiresUpdate = updateStrategy.requiresUpdate(t, cached.hasSellPrice() ? cached.sellPrice() : null); break; case ANY: requiresUpdate = (updateStrategy.requiresUpdate(t, cached.hasBuyPrice() ? cached.buyPrice() : null) || updateStrategy.requiresUpdate(t, cached.hasSellPrice() ? cached.sellPrice() : null)); break; default: throw new RuntimeException("Unhandled switch/case: " + filter.getOrderType()); } if (LOG.isDebugEnabled()) { LOG.debug("getPriceInfos(): [ " + updateStrategy + "] requires_update => " + requiresUpdate + " , type=" + t.getName()); } if (requiresUpdate) { params.add(new BasicNameValuePair("typeid", t.getId().toString())); } } } }); if (params.isEmpty() || isOfflineMode()) { // all entries served from cache return resultHolder.get(); } addFilterToRequest(params, filter); /* * Query data from eve central */ final String responseXmlFromServer = eveCentralClient.sendRequestToServer(params); final AtomicReference<String> xml = new AtomicReference<String>(responseXmlFromServer); /* * NEEDS to be run on the EDT since Hibernate * lazy-fetching might kick in and * the Hibernate session is confined to the EDT. */ return runOnEventThread(new PriceCallable() { public Map<InventoryType, PriceInfoQueryResult> call() throws PriceInfoUnavailableException { final Map<InventoryType, PriceInfoQueryResult> realResult = resultHolder.get(); final Map<Long, List<PriceInfo>> result = parsePriceInfo(filter, xml.get()); // group prices by item types List<PriceInfo> updated = new ArrayList<>(); try { for (InventoryType type : items) { List<PriceInfo> info = result.get(type.getId()); if (info == null || info.isEmpty()) { // failed to fetch data, query user rememberPriceMissingOnEveCentral(filter, type); info = queryPriceFromUser(filter, callback, type); } forgetPriceMissingOnEveCentral(filter, type); for (PriceInfo dataFromServer : info) { dataFromServer.setRegion(filter.getRegion()); dataFromServer.setTimestamp(new EveDate(systemClock)); dataFromServer.setInventoryType(type); final PriceInfoQueryResult cachedResult = realResult.get(type); if (LOG.isDebugEnabled()) { LOG.debug("getPriceInfos(): from server: " + dataFromServer + " , cached=" + cachedResult); } PriceInfo existing; switch (filter.getOrderType()) { case BUY: existing = cachedResult.hasBuyPrice() ? cachedResult.buyPrice() : null; if (updateStrategy.requiresUpdate(type, existing)) { LOG.debug("getPriceInfos(): merging buy price."); realResult.put(type, cachedResult.merge(filter.getOrderType(), dataFromServer)); storeCacheEntry(dataFromServer); updated.add(dataFromServer); } break; case SELL: existing = cachedResult.hasSellPrice() ? cachedResult.sellPrice() : null; if (updateStrategy.requiresUpdate(type, existing)) { LOG.debug("getPriceInfos(): merging sell price."); realResult.put(type, cachedResult.merge(filter.getOrderType(), dataFromServer)); storeCacheEntry(dataFromServer); updated.add(dataFromServer); } break; case ANY: existing = cachedResult.hasBuyPrice() ? cachedResult.buyPrice() : null; if (updateStrategy.requiresUpdate(type, existing)) { LOG.debug("getPriceInfos(): merging buy price."); realResult.put(type, cachedResult.merge(PriceInfo.Type.BUY, dataFromServer)); storeCacheEntry(dataFromServer); updated.add(dataFromServer); } existing = cachedResult.hasSellPrice() ? cachedResult.sellPrice() : null; if (updateStrategy.requiresUpdate(type, existing)) { LOG.debug("getPriceInfos(): merging sell price."); realResult.put(type, cachedResult.merge(PriceInfo.Type.SELL, dataFromServer)); storeCacheEntry(dataFromServer); updated.add(dataFromServer); } break; default: throw new RuntimeException("Unhandled switch/case: " + filter.getOrderType()); } } } } finally { fireItemPriceChanged(updated); } return realResult; } }); } private interface PriceCallable { public Map<InventoryType, PriceInfoQueryResult> call() throws PriceInfoUnavailableException; } private static Map<InventoryType, PriceInfoQueryResult> runOnEventThread(final PriceCallable r) throws PriceInfoUnavailableException { if (SwingUtilities.isEventDispatchThread()) { return r.call(); } final AtomicReference<Map<InventoryType, PriceInfoQueryResult>> result = new AtomicReference<Map<InventoryType, PriceInfoQueryResult>>(); try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { try { result.set(r.call()); } catch (PriceInfoUnavailableException e) { throw new RuntimeException(e); } } }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (InvocationTargetException e) { Throwable wrapped = e.getTargetException(); if (wrapped instanceof RuntimeException) { if (wrapped.getCause() instanceof PriceInfoUnavailableException) { throw (PriceInfoUnavailableException) wrapped.getCause(); } throw (RuntimeException) wrapped; } else if (e.getTargetException() instanceof Error) { throw (Error) wrapped; } throw new RuntimeException(e.getTargetException()); } return result.get(); } private List<PriceInfo> queryPriceFromUser(MarketFilter filter, IPriceQueryCallback callback, InventoryType type) throws PriceInfoUnavailableException { if (callback != null) { return callback.getPriceInfo(filter, "Please enter cost information:", type); } LOG.error("getPriceInfos(): Unable to obtain price for item " + type.getId()); throw new PriceInfoUnavailableException("Unable to obtain price", type); } private void addFilterToRequest(List<NameValuePair> params, MarketFilter filter) { if (filter.isFilterByMinQuantity()) { params.add(new BasicNameValuePair("minQ", Long.toString(filter.getMinQuantity()))); } params.add(new BasicNameValuePair("regionlimit", Long.toString(filter.getRegion().getID()))); } protected Map<Long, List<PriceInfo>> parsePriceInfo(MarketFilter filter, String xmlResponse) { if (LOG.isDebugEnabled()) { LOG.debug("parsePriceInfo(): filter = " + filter); } final Document document = parseXML(xmlResponse); final XPathExpression TYPE_NODES = super.compileXPathExpression("/evec_api/marketstat/type"); final Map<Long, List<PriceInfo>> result = new HashMap<Long, List<PriceInfo>>(); for (Element node : super.selectElements(document, TYPE_NODES)) { final Long typeId = getLongAttributeValue(node, "id"); Element itemNode; switch (filter.getOrderType()) { case BUY: itemNode = getChild(node, "buy"); addToMap(result, parsePriceInfo(typeId, PriceInfo.Type.BUY, itemNode)); break; case SELL: itemNode = getChild(node, "sell"); addToMap(result, parsePriceInfo(typeId, PriceInfo.Type.SELL, itemNode)); break; case ANY: itemNode = getChild(node, "buy"); addToMap(result, parsePriceInfo(typeId, PriceInfo.Type.BUY, itemNode)); itemNode = getChild(node, "sell"); addToMap(result, parsePriceInfo(typeId, PriceInfo.Type.SELL, itemNode)); break; default: throw new RuntimeException("Unhandled order type: " + filter.getOrderType()); } } return result; } protected static void addToMap(Map<Long, List<PriceInfo>> map, PriceInfoWithId info) { if (info.getAveragePrice() <= 0) { return; } List<PriceInfo> existing = map.get(info.getTypeID()); if (existing == null) { existing = new ArrayList<PriceInfo>(); map.put(info.getTypeID(), existing); } existing.add(info); } protected static class PriceInfoWithId extends PriceInfo { private Long itemType; protected PriceInfoWithId(Long itemType, Type infoType) { super(infoType, Source.EVE_CENTRAL); if (itemType == null) { throw new IllegalArgumentException("itemType cannot be NULL"); } this.itemType = itemType; } public Long getTypeID() { return itemType; } } private PriceInfoWithId parsePriceInfo(Long itemTypeId, PriceInfo.Type priceType, Element rootNode) { /* <all> <volume>41337268582</volume> <avg>36.8872779152</avg> <max>11155.08</max> <min>0.3</min> <stddev>552.601683331</stddev> <median>4.15</median> </all> */ final PriceInfoWithId result = new PriceInfoWithId(itemTypeId, priceType); result.setAveragePrice(parseISKValue(getChildValue(rootNode, "median"))); result.setMinPrice(parseISKValue(getChildValue(rootNode, "min"))); result.setMaxPrice(parseISKValue(getChildValue(rootNode, "max"))); return result; } protected long parseISKValue(String value) { final Double dValue = Double.parseDouble(value); return Math.round(dValue * 100.0f); } public void setPriceInfoStore(IPriceInfoStore priceInfoStore) { this.priceInfoStore = priceInfoStore; } @Override public void updatePriceInfo(MarketFilter filter, List<InventoryType> items, IPriceQueryCallback callback, IUpdateStrategy updateStrategy) throws PriceInfoUnavailableException { if (filter == null) { throw new IllegalArgumentException("filter cannot be NULL"); } if (items == null) { throw new IllegalArgumentException("items cannot be NULL"); } if (updateStrategy == null) { throw new IllegalArgumentException("updateStrategy cannot be NULL"); } if (LOG.isDebugEnabled()) { LOG.debug("updatePriceInfo(): filter = " + filter + ", " + " , items = " + items + " , update_strategy = " + updateStrategy); } for (InventoryType type : items) { if (LOG.isTraceEnabled()) { LOG.debug("updatePriceInfo(): querying price for " + type); } getPriceInfo(filter, callback, type); } } @Override public void addChangeListener(IPriceInfoChangeListener listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be NULL"); } synchronized (listeners) { listeners.add(listener); } } protected void fireItemPriceChanged(PriceInfo info) { fireItemPriceChanged(Collections.singleton(info)); } protected void fireItemPriceChanged(Collection<PriceInfo> infos) { final Map<Region, Set<InventoryType>> infosByRegion = new HashMap<>(); for (PriceInfo i : infos) { Set<InventoryType> list = infosByRegion.get(i.getRegion()); if (list == null) { list = new HashSet<>(); infosByRegion.put(i.getRegion(), list); } list.add(i.getItemType()); } for (Entry<Region, Set<InventoryType>> entry : infosByRegion.entrySet()) { fireItemPriceChanged(entry.getKey(), entry.getValue()); } } protected void fireItemPriceChanged(Region region, Set<InventoryType> items) { LOG.debug("fireItemPriceChanged(): items = " + items); final List<IPriceInfoChangeListener> cloned; synchronized (listeners) { cloned = new ArrayList<IPriceInfoChangeListener>(listeners); } for (IPriceInfoChangeListener listener : cloned) { listener.priceChanged(this, region, items); } } @Override public void removeChangeListener(IPriceInfoChangeListener listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be NULL"); } synchronized (listeners) { listeners.add(listener); } } protected boolean isOutdated(PriceInfo info) { return PriceInfoQueryResult.isOutdated(info, systemClock); } @Override public IUpdateStrategy createUpdateStrategy(UpdateMode mode, PriceInfo.Type typeFilter) { switch (mode) { case DEFAULT: return new DEFAULT_STRATEGY(typeFilter); case UPDATE_ALL: return new UPDATE_ALL_STRATEGY(typeFilter); case UPDATE_MISSING: return new UPDATE_MISSING_STRATEGY(typeFilter); case UPDATE_NONE: return new UPDATE_NONE_STRATEGY(); case UPDATE_OUTDATED: return new UPDATE_OUTDATED_STRATEGY(typeFilter); case UPDATE_USERPROVIDED: return new UpdateUserProvidedPrices(typeFilter); case UPDATE_MISSING_OR_OUTDATED: return new UpdateMissingOrOutdatedPrices(typeFilter); default: throw new RuntimeException("Unhandled switch/case: " + mode); } } @Override public void addStatusCallback(IStatusCallback callback) { this.eveCentralClient.addStatusCallback(callback); } public void setEveCentralClient(IEveCentralClient client) { if (client == null) { throw new IllegalArgumentException("client cannot be NULL"); } this.eveCentralClient = client; } @Override public void dispose() { try { this.priceInfoStore.persist(); } catch (IOException e) { LOG.error("dispose(): Failed to persist price info store ?"); } this.eveCentralClient.dispose(); } @Override public void removeStatusCallback(IStatusCallback callback) { this.eveCentralClient.removeStatusCallback(callback); } @Override public void destroy() throws Exception { dispose(); } @Override public boolean isOfflineMode() { return isOfflineMode; } @Override public void setOfflineMode(boolean yesNo) { LOG.info("setOfflineMode(): offline = " + yesNo); this.isOfflineMode = yesNo; } @Override public void store(Collection<PriceInfo> info) { for (PriceInfo i : info) { store(i); } } @Override public void store(PriceInfo info) { this.priceInfoStore.save(info); fireItemPriceChanged(info); } public Map<Long, InventoryType> getAllKnownInventoryTypes(Region region, IInventoryTypeDAO dao) { return this.priceInfoStore.getAllKnownInventoryTypes(region, dao); } }