package org.apache.solr.schema; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.apache.noggit.JSONParser; import org.apache.lucene.analysis.util.ResourceLoader; import org.apache.lucene.util.IOUtils; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Exchange Rates Provider for {@link CurrencyField} implementing the freely available * exchange rates from openexchangerates.org * <p/> * <b>Disclaimer:</b> This data is collected from various providers and provided free of charge * for informational purposes only, with no guarantee whatsoever of accuracy, validity, * availability or fitness for any purpose; use at your own risk. Other than that - have * fun, and please share/watch/fork if you think data like this should be free! */ public class OpenExchangeRatesOrgProvider implements ExchangeRateProvider { public static Logger log = LoggerFactory.getLogger(OpenExchangeRatesOrgProvider.class); protected static final String PARAM_RATES_FILE_LOCATION = "ratesFileLocation"; protected static final String PARAM_REFRESH_INTERVAL = "refreshInterval"; protected static final String DEFAULT_RATES_FILE_LOCATION = "http://openexchangerates.org/latest.json"; protected static final String DEFAULT_REFRESH_INTERVAL = "1440"; protected String ratesFileLocation; protected int refreshInterval; protected ResourceLoader resourceLoader; protected OpenExchangeRates rates; /** * Returns the currently known exchange rate between two currencies. The rates are fetched from * the freely available OpenExchangeRates.org JSON, hourly updated. All rates are symmetrical with * base currency being USD by default. * * @param sourceCurrencyCode The source currency being converted from. * @param targetCurrencyCode The target currency being converted to. * @return The exchange rate. * @throws SolrException if the requested currency pair cannot be found */ public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) { if (rates == null) { throw new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE, "Rates not initialized."); } if (sourceCurrencyCode == null || targetCurrencyCode == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cannot get exchange rate; currency was null."); } if (rates.getTimestamp() + refreshInterval*60*1000 > System.currentTimeMillis()) { log.debug("Refresh interval has expired. Refreshing exchange rates."); reload(); } Double source = (Double) rates.getRates().get(sourceCurrencyCode); Double target = (Double) rates.getRates().get(targetCurrencyCode); if (source == null || target == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No available conversion rate from " + sourceCurrencyCode + " to " + targetCurrencyCode + ". " + "Available rates are "+listAvailableCurrencies()); } return target / source; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OpenExchangeRatesOrgProvider that = (OpenExchangeRatesOrgProvider) o; return !(rates != null ? !rates.equals(that.rates) : that.rates != null); } @Override public int hashCode() { return rates != null ? rates.hashCode() : 0; } public String toString() { return "["+this.getClass().getName()+" : " + rates.getRates().size() + " rates.]"; } @Override public Set<String> listAvailableCurrencies() { if (rates == null) throw new SolrException(ErrorCode.SERVER_ERROR, "Rates not initialized"); return rates.getRates().keySet(); } @Override public boolean reload() throws SolrException { InputStream ratesJsonStream = null; try { log.info("Reloading exchange rates from "+ratesFileLocation); try { ratesJsonStream = (new URL(ratesFileLocation)).openStream(); } catch (Exception e) { ratesJsonStream = resourceLoader.openResource(ratesFileLocation); } rates = new OpenExchangeRates(ratesJsonStream); return true; } catch (Exception e) { throw new SolrException(ErrorCode.SERVER_ERROR, "Error reloading exchange rates", e); } finally { if (ratesJsonStream != null) try { ratesJsonStream.close(); } catch (IOException e) { throw new SolrException(ErrorCode.SERVER_ERROR, "Error closing stream", e); } } } @Override public void init(Map<String,String> params) throws SolrException { try { ratesFileLocation = getParam(params.get(PARAM_RATES_FILE_LOCATION), DEFAULT_RATES_FILE_LOCATION); refreshInterval = Integer.parseInt(getParam(params.get(PARAM_REFRESH_INTERVAL), DEFAULT_REFRESH_INTERVAL)); // Force a refresh interval of minimum one hour, since the API does not offer better resolution if (refreshInterval < 60) { refreshInterval = 60; log.warn("Specified refreshInterval was too small. Setting to 60 minutes which is the update rate of openexchangerates.org"); } log.info("Initialized with rates="+ratesFileLocation+", refreshInterval="+refreshInterval+"."); } catch (Exception e) { throw new SolrException(ErrorCode.BAD_REQUEST, "Error initializing", e); } finally { // Removing config params custom to us params.remove(PARAM_RATES_FILE_LOCATION); params.remove(PARAM_REFRESH_INTERVAL); } } @Override public void inform(ResourceLoader loader) throws SolrException { resourceLoader = loader; reload(); } private String getParam(String param, String defaultParam) { return param == null ? defaultParam : param; } /** * A simple class encapsulating the JSON data from openexchangerates.org */ class OpenExchangeRates { private Map<String, Double> rates; private String baseCurrency; private long timestamp; private String disclaimer; private String license; private JSONParser parser; public OpenExchangeRates(InputStream ratesStream) throws IOException { parser = new JSONParser(new InputStreamReader(ratesStream, IOUtils.CHARSET_UTF_8)); rates = new HashMap<String, Double>(); int ev; do { ev = parser.nextEvent(); switch( ev ) { case JSONParser.STRING: if( parser.wasKey() ) { String key = parser.getString(); if(key.equals("disclaimer")) { parser.nextEvent(); disclaimer = parser.getString(); } else if(key.equals("license")) { parser.nextEvent(); license = parser.getString(); } else if(key.equals("timestamp")) { parser.nextEvent(); timestamp = parser.getLong(); } else if(key.equals("base")) { parser.nextEvent(); baseCurrency = parser.getString(); } else if(key.equals("rates")) { ev = parser.nextEvent(); assert(ev == JSONParser.OBJECT_START); ev = parser.nextEvent(); while (ev != JSONParser.OBJECT_END) { String curr = parser.getString(); ev = parser.nextEvent(); Double rate = parser.getDouble(); rates.put(curr, rate); ev = parser.nextEvent(); } } else { log.warn("Unknown key "+key); } break; } else { log.warn("Expected key, got "+JSONParser.getEventString(ev)); break; } case JSONParser.OBJECT_END: case JSONParser.OBJECT_START: case JSONParser.EOF: break; default: log.info("Noggit UNKNOWN_EVENT_ID:"+JSONParser.getEventString(ev)); break; } } while( ev != JSONParser.EOF); } public Map<String, Double> getRates() { return rates; } public long getTimestamp() { return timestamp; } public String getDisclaimer() { return disclaimer; } public String getBaseCurrency() { return baseCurrency; } public String getLicense() { return license; } } }