/* =================================================================== * DelimitedPriceDatumDataSource.java * * Created Aug 8, 2009 2:09:30 PM * * Copyright (c) 2009 Solarnetwork.net Dev Team. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * =================================================================== */ package net.solarnetwork.node.price.delimited; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import net.solarnetwork.node.DatumDataSource; import net.solarnetwork.node.domain.GeneralLocationDatum; import net.solarnetwork.node.domain.GeneralPriceDatum; import net.solarnetwork.node.domain.PriceDatum; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; /** * Implementation of {@link DatumDataSource} that parses a delimited text * resource from a URL. * * <p> * This class will make a URL request and parse the returned text as delimited * lines of data. The references to <em>columns</em> in the class properties * refer to zero-based column numbers created after splitting the line of data * into an array using the configured delimiter. * </p> * * <p> * The configurable properties of this class are: * </p> * * <dl class="class-properties"> * <dt>url</dt> * <dd>The URL template for accessing the delimited price data from. This will * be passed through {@link String#format(String, Object...)} with the current * date as the only parameter, allowing the URL to contain a date requeset * parameter if needed. For example, a value of * {@code http://some.place/prices?date=%1$tY-%1$tm-%1$td} would resolve to * something like {@code http://some.place/prices?date=2009-08-08}.</dd> * * <dt>delimiter</dt> * <dd>A regular expression delimiter to split the lines of text with. Defaults * to {@link #DEFAULT_DELIMITER}.</dd> * * <dt>skipLines</dt> * <dd>The number of lines of text to skip. This is useful for skipping a * "header" row with column names. Defaults to {@code 1}.</dd> * * <dt>connectionTimeout</dt> * <dd>A URL connection timeout to apply when requesting the data. Defaults to * {@link #DEFAULT_CONNECTION_TIMEOUT}.</dd> * * <dt>priceColumn</dt> * <dd>The result column index for the price. This is assumed to be parsable as * a double value.</dd> * * <dt>sourceIdColumn</dt> * <dd>An optional column index to use for the {@link PriceDatum#getSourceId()} * value. If not configured, the URL used to request the data will be used.</dd> * * <dt>dateTimeColumns</dt> * <dd>An array of column indices to use as the {@link PriceDatum#getCreated()} * value. This is provided as an array in case the date and time of the price is * split across multiple columns. If multiple columns are configured, they will * be joined with a space character before parsing the result into a Date * object.</dd> * * <dt>dateFormat</dt> * <dd>The {@link SimpleDateFormat} format to use for parsing the price date * value into a Date object. Defaults to {@link #DEFAULT_DATE_FORMAT}.</dd> * </dl> * * @author matt * @version 1.2 */ public class DelimitedPriceDatumDataSource implements DatumDataSource<GeneralLocationDatum>, SettingSpecifierProvider { /** The default value for the {@code delimiter} property. */ public static final String DEFAULT_DELIMITER = ","; /** The default value for the {@code connectionTimeout} property. */ public static final int DEFAULT_CONNECTION_TIMEOUT = 15000; /** The default value for the {@code dateFormat} property. */ public static final String DEFAULT_DATE_FORMAT = "dd/MM/yyyy HH:mm"; /** The default value for the {@code url} property. */ public static final String DEFAULT_URL = "https://www.electricityinfo.co.nz/comitFta/five_min_prices.download?INchoice=HAY&INdate=%1$td/%1$tm/%1$tY&INgip=ABY0111&INperiodfrom=1&INperiodto=50&INtype=Price"; /** The default value for the {@code priceColumn} property. */ public static final int DEFAULT_PRICE_COLUMN = 4; /** The default value for the {@code skipLines} property. */ public static final int DEFAULT_SKIP_LINES = 1; private static final int[] DEFAULT_DATE_TIME_COLUMNS = new int[] { 1, 3 }; private final Logger log = LoggerFactory.getLogger(DelimitedPriceDatumDataSource.class); private MessageSource messageSource; private String url = DEFAULT_URL; private String delimiter = DEFAULT_DELIMITER; private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; private int skipLines = DEFAULT_SKIP_LINES; private int[] dateTimeColumns = DEFAULT_DATE_TIME_COLUMNS; private int priceColumn = DEFAULT_PRICE_COLUMN; private String dateFormat = DEFAULT_DATE_FORMAT; private String uid = null; private String groupUID = null; @Override public Class<? extends GeneralLocationDatum> getDatumType() { return GeneralPriceDatum.class; } @Override public String toString() { String host = ""; try { URL theUrl = getFormattedUrl(); host = theUrl.getHost(); } catch ( Exception e ) { host = "unknown"; } return "DelimitedPriceDatumDataSource{" + host + "}"; } @Override public GeneralLocationDatum readCurrentDatum() { URL theUrl = getFormattedUrl(); String dataRow = readDataRow(theUrl); if ( dataRow == null ) { return null; } String[] data = dataRow.split(this.delimiter); // get price date, either from single column or combination of multiple // which might occur if date and time are in different columns String dateTimeStr = null; if ( dateTimeColumns.length == 1 ) { dateTimeStr = data[dateTimeColumns[0]]; } else { StringBuilder buf = new StringBuilder(); for ( int idx : dateTimeColumns ) { if ( buf.length() > 0 ) { buf.append(' '); } buf.append(data[idx]); } dateTimeStr = buf.toString(); } if ( log.isTraceEnabled() ) { log.trace("Parsing price date [" + dateTimeStr + ']'); } SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); Date created; try { created = sdf.parse(dateTimeStr); } catch ( ParseException e ) { throw new RuntimeException(e); } BigDecimal price = new BigDecimal(data[priceColumn]); GeneralPriceDatum datum = new GeneralPriceDatum(); datum.setCreated(created); datum.setPrice(price); return datum; } private URL getFormattedUrl() { String theUrl = String.format(this.url, new Date()); try { return new URL(theUrl); } catch ( MalformedURLException e ) { throw new RuntimeException(e); } } private String readDataRow(URL theUrl) { BufferedReader resp = null; if ( log.isDebugEnabled() ) { log.debug("Requesting price data from [{}]", theUrl); } try { URLConnection conn = theUrl.openConnection(); conn.setConnectTimeout(this.connectionTimeout); conn.setReadTimeout(this.connectionTimeout); conn.setRequestProperty("Accept", "text/*"); resp = new BufferedReader(new InputStreamReader(conn.getInputStream())); if ( conn instanceof HttpURLConnection ) { HttpURLConnection hconn = (HttpURLConnection) conn; int status = hconn.getResponseCode(); if ( status < 200 || status > 299 ) { log.warn("Non-200 response {} from [{}]; headers:\n", status, theUrl, conn.getHeaderFields()); return null; } } String str; int skipCount = this.skipLines; while ( (str = resp.readLine()) != null ) { if ( skipCount > 0 ) { skipCount--; continue; } break; } if ( log.isTraceEnabled() ) { log.trace("Found price data: {}", str); } return str; } catch ( IOException e ) { throw new RuntimeException(e); } finally { if ( resp != null ) { try { resp.close(); } catch ( IOException e ) { // ignore this log.debug("Exception closing URL stream", e); } } } } @Override public String getSettingUID() { return "net.solarnetwork.node.price.delimited"; } @Override public String getDisplayName() { return "Delimited energy price lookup"; } @Override public MessageSource getMessageSource() { return messageSource; } public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } @Override public List<SettingSpecifier> getSettingSpecifiers() { return Arrays.asList((SettingSpecifier) new BasicTextFieldSettingSpecifier("url", DEFAULT_URL), (SettingSpecifier) new BasicTextFieldSettingSpecifier("uid", null), (SettingSpecifier) new BasicTextFieldSettingSpecifier("groupUID", null), (SettingSpecifier) new BasicTextFieldSettingSpecifier("delimiter", DEFAULT_DELIMITER), (SettingSpecifier) new BasicTextFieldSettingSpecifier("priceColumn", String.valueOf(DEFAULT_PRICE_COLUMN)), (SettingSpecifier) new BasicTextFieldSettingSpecifier("dateTimeColumns", "1,3"), (SettingSpecifier) new BasicTextFieldSettingSpecifier("dateFormat", DEFAULT_DATE_FORMAT), (SettingSpecifier) new BasicTextFieldSettingSpecifier("skipLines", String.valueOf(DEFAULT_SKIP_LINES))); } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getDelimiter() { return delimiter; } public void setDelimiter(String delimiter) { this.delimiter = delimiter; } public int getConnectionTimeout() { return connectionTimeout; } public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } public int getSkipLines() { return skipLines; } public void setSkipLines(int skipLines) { this.skipLines = skipLines; } public int[] getDateTimeColumns() { return dateTimeColumns; } public void setDateTimeColumns(int[] dateTimeColumns) { this.dateTimeColumns = dateTimeColumns; } public int getPriceColumn() { return priceColumn; } public void setPriceColumn(int priceColumn) { this.priceColumn = priceColumn; } public String getDateFormat() { return dateFormat; } public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } @Override public String getUID() { return getUid(); } public String getUid() { return uid; } public void setUid(String uid) { this.uid = uid; } @Override public String getGroupUID() { return groupUID; } public void setGroupUID(String groupUID) { this.groupUID = groupUID; } }