| /* |
| * 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. |
| */ |
| package org.apache.solr.schema; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.lang.invoke.MethodHandles; |
| import java.net.URL; |
| import java.nio.charset.StandardCharsets; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.solr.common.util.SuppressForbidden; |
| import org.noggit.JSONParser; |
| import org.apache.lucene.analysis.util.ResourceLoader; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.SolrException.ErrorCode; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * <p> |
| * Exchange Rates Provider for {@link CurrencyField} and {@link CurrencyFieldType} capable of fetching & |
| * parsing the freely available exchange rates from openexchangerates.org |
| * </p> |
| * <p> |
| * Configuration Options: |
| * </p> |
| * <ul> |
| * <li><code>ratesFileLocation</code> - A file path or absolute URL specifying the JSON data to load (mandatory)</li> |
| * <li><code>refreshInterval</code> - How frequently (in minutes) to reload the exchange rate data (default: 1440)</li> |
| * </ul> |
| * <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! |
| * </p> |
| * @see <a href="https://openexchangerates.org/documentation">openexchangerates.org JSON Data Format</a> |
| */ |
| public class OpenExchangeRatesOrgProvider implements ExchangeRateProvider { |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| protected static final String PARAM_RATES_FILE_LOCATION = "ratesFileLocation"; |
| protected static final String PARAM_REFRESH_INTERVAL = "refreshInterval"; |
| protected static final String DEFAULT_REFRESH_INTERVAL = "1440"; |
| |
| protected String ratesFileLocation; |
| // configured in minutes, but stored in seconds for quicker math |
| protected int refreshIntervalSeconds; |
| 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 |
| */ |
| @Override |
| 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."); |
| } |
| |
| reloadIfExpired(); |
| |
| Double source = rates.getRates().get(sourceCurrencyCode); |
| Double target = 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; |
| } |
| |
| @SuppressForbidden(reason = "Need currentTimeMillis, for comparison with stamp in an external file") |
| private void reloadIfExpired() { |
| if ((rates.getTimestamp() + refreshIntervalSeconds)*1000 < System.currentTimeMillis()) { |
| log.debug("Refresh interval has expired. Refreshing exchange rates."); |
| reload(); |
| } |
| } |
| |
| @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; |
| } |
| |
| @Override |
| 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 |
| @SuppressWarnings("resource") |
| public boolean reload() throws SolrException { |
| InputStream ratesJsonStream = null; |
| try { |
| log.debug("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 = params.get(PARAM_RATES_FILE_LOCATION); |
| if (null == ratesFileLocation) { |
| throw new SolrException(ErrorCode.SERVER_ERROR, "Init param must be specified: " + PARAM_RATES_FILE_LOCATION); |
| } |
| int 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.debug("Initialized with rates={}, refreshInterval={}.", ratesFileLocation, refreshInterval); |
| refreshIntervalSeconds = refreshInterval * 60; |
| } catch (SolrException e1) { |
| throw e1; |
| } catch (Exception e2) { |
| throw new SolrException(ErrorCode.SERVER_ERROR, "Error initializing: " + |
| e2.getMessage(), e2); |
| } 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 |
| */ |
| static 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, StandardCharsets.UTF_8)); |
| rates = new HashMap<>(); |
| |
| 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: |
| if (log.isInfoEnabled()) { |
| 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; |
| } |
| /** Package protected method for test purposes |
| * @lucene.internal |
| */ |
| void setTimestamp(long timestamp) { |
| this.timestamp = timestamp; |
| } |
| |
| public String getDisclaimer() { |
| return disclaimer; |
| } |
| |
| public String getBaseCurrency() { |
| return baseCurrency; |
| } |
| |
| public String getLicense() { |
| return license; |
| } |
| } |
| } |