/* ==================================================================
* LocationDatumDataSource.java - Feb 21, 2011 5:23:28 PM
*
* Copyright 2007-2011 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.support;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import net.solarnetwork.domain.GeneralLocationSourceMetadata;
import net.solarnetwork.node.DatumDataSource;
import net.solarnetwork.node.LocationService;
import net.solarnetwork.node.MultiDatumDataSource;
import net.solarnetwork.node.domain.BasicGeneralLocation;
import net.solarnetwork.node.domain.Datum;
import net.solarnetwork.node.domain.GeneralLocation;
import net.solarnetwork.node.domain.GeneralLocationDatum;
import net.solarnetwork.node.domain.GeneralNodeDatum;
import net.solarnetwork.node.domain.Location;
import net.solarnetwork.node.domain.PriceLocation;
import net.solarnetwork.node.domain.PricedDatum;
import net.solarnetwork.node.settings.KeyedSettingSpecifier;
import net.solarnetwork.node.settings.LocationLookupSettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicLocationLookupSettingSpecifier;
import net.solarnetwork.node.util.PrefixedMessageSource;
import net.solarnetwork.util.OptionalService;
import net.solarnetwork.util.OptionalServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
/**
* {@link DatumDataSource} that augments some other data source's datum values
* with location IDs.
*
* <p>
* This is to be used to easily augment various datum that relate to a location
* with the necessary {@link Location#getLocationId()} ID. This class also
* implements the {@link MultiDatumDataSource} API, and will call the methods of
* that API on the configured {@code delegate} if that also implements
* {@link MultiDatumDataSource}. If the {@code delegate} does not implement
* {@link MultiDatumDataSource} this class will "fake" that API by calling
* {@link DatumDataSource#readCurrentDatum()} and returning that object in a
* Collection.
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>delegate</dt>
* <dd>The {@link DatumDataSource} to delegate to.</dd>
*
* <dt>locationType</dt>
* <dd>The type of location to search for. Defaults to {@link PriceLocation}.</dd>
*
* <dt>locationService</dt>
* <dd>The {@link LocationService} to use to lookup {@link Location} instances
* via the configured {@code locationId} property.</dd>
*
* <dt>locationId</dt>
* <dd>The {@link Location} ID to assign.</dd>
*
* <dt>locationIdPropertyName</dt>
* <dd>The JavaBean property name to set the found
* {@link Location#getLocationId()} to on the {@link Datum} returned from the
* configured {@code delegate}. The object must support a JavaBean setter method
* for this property. Defaults to {@link #DEFAULT_LOCATION_ID_PROP_NAME}.</dd>
*
* <dt>sourceIdId</dt>
* <dd>The location source ID to assign.</dd>
*
* <dt>sourceIdPropertyName</dt>
* <dd>The JavaBean property name to set the found
* {@link Location#getSourceId()} to on the {@link Datum} returned from the
* configured {@code delegate}. The object must support a JavaBean setter method
* for this property. Defaults to {@link #DEFAULT_SOURCE_ID_PROP_NAME}.</dd>
*
* <dt>requireLocationService</dt>
* <dd>If configured as <em>true</em> then return <em>null</em> data only
* instead of calling the delegate. This is designed for services that require a
* location ID to be set, for example a Location Datum logger. Defaults to
* <em>false</em>.</dd>
*
* <dt>messageBundleBasename</dt>
* <dd>The message bundle basename to use. This can be customized so different
* messages can be shown for different uses of this proxy. Defaults to
* {@link #PRICE_LOCATION_MESSAGE_BUNDLE}.</dd>
* </dl>
*
* @author matt
* @version 1.5
*/
public class LocationDatumDataSource<T extends Datum> implements DatumDataSource<T>,
MultiDatumDataSource<T>, SettingSpecifierProvider {
/** Default value for the {@code locationIdPropertyName} property. */
public static final String DEFAULT_LOCATION_ID_PROP_NAME = "locationId";
/** Default value for the {@code sourceIdPropertyName} property. */
public static final String DEFAULT_SOURCE_ID_PROP_NAME = "locationSourceId";
/** Bundle name for price location lookup messages. */
public static final String PRICE_LOCATION_MESSAGE_BUNDLE = "net.solarnetwork.node.support.PriceLocationDatumDataSource";
private DatumDataSource<T> delegate;
private OptionalService<LocationService> locationService;
private String locationType = Location.PRICE_TYPE;
private String locationIdPropertyName = DEFAULT_LOCATION_ID_PROP_NAME;
private String sourceIdPropertyName = DEFAULT_SOURCE_ID_PROP_NAME;
private boolean requireLocationService = false;
private String messageBundleBasename = PRICE_LOCATION_MESSAGE_BUNDLE;
private Long locationId = null;
private String sourceId = null;
private Set<String> datumClassNameIgnore;
private GeneralLocation location = null;
private MessageSource messageSource;
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* Factory method.
*
* <p>
* This method exists to work around issues with wiring this class via
* Gemini Blueprint 2.2. It throws a
* {@code SpringBlueprintConverterService$BlueprintConverterException} if
* the delegate parameter is defined as {@code DatumDataSource}.
* </p>
*
* @param delegate
* the delegate, must implement
* {@code DatumDataSource<? extends Datum>}
* @param locationService
* the location service
* @return the data source
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static LocationDatumDataSource<? extends Datum> getInstance(Object delegate,
OptionalServiceTracker<LocationService> locationService) {
LocationDatumDataSource<? extends Datum> ds = new LocationDatumDataSource<Datum>();
ds.setDelegate((DatumDataSource) delegate);
ds.setLocationService(locationService);
return ds;
}
@Override
public Class<? extends T> getDatumType() {
return delegate.getDatumType();
}
@SuppressWarnings("unchecked")
@Override
public Class<? extends T> getMultiDatumType() {
if ( delegate instanceof MultiDatumDataSource ) {
return ((MultiDatumDataSource<T>) delegate).getMultiDatumType();
}
return delegate.getDatumType();
}
@SuppressWarnings("unchecked")
@Override
public Collection<T> readMultipleDatum() {
Collection<T> results = null;
if ( delegate instanceof MultiDatumDataSource ) {
results = ((MultiDatumDataSource<T>) delegate).readMultipleDatum();
} else {
// fake multi API
results = new ArrayList<T>(1);
T datum = delegate.readCurrentDatum();
if ( datum != null ) {
results.add(datum);
}
}
if ( results != null && locationId != null ) {
for ( T datum : results ) {
populateLocation(datum);
}
} else if ( results != null && results.size() > 0 && locationId == null
&& requireLocationService ) {
log.warn("Location required but not available, discarding datum: {}", results);
results = Collections.emptyList();
}
return results;
}
@Override
public T readCurrentDatum() {
T datum = delegate.readCurrentDatum();
if ( datum != null && locationId != null ) {
populateLocation(datum);
} else if ( datum != null && locationId == null && requireLocationService ) {
log.warn("LocationService required but not available, discarding datum: {}", datum);
datum = null;
}
return datum;
}
private void populateLocation(T datum) {
if ( locationId != null && sourceId != null && !shouldIgnoreDatum(datum) ) {
log.debug("Augmenting datum {} with locaiton ID {} ({})", datum, locationId, sourceId);
if ( datum instanceof GeneralLocationDatum ) {
GeneralLocationDatum gDatum = (GeneralLocationDatum) datum;
gDatum.setLocationId(locationId);
gDatum.setSourceId(sourceId);
} else if ( datum instanceof GeneralNodeDatum ) {
GeneralNodeDatum gDatum = (GeneralNodeDatum) datum;
gDatum.putStatusSampleValue(PricedDatum.PRICE_LOCATION_KEY, locationId);
gDatum.putStatusSampleValue(PricedDatum.PRICE_SOURCE_KEY, sourceId);
} else {
BeanWrapper bean = PropertyAccessorFactory.forBeanPropertyAccess(datum);
if ( bean.isWritableProperty(locationIdPropertyName)
&& bean.isWritableProperty(sourceIdPropertyName) ) {
bean.setPropertyValue(locationIdPropertyName, locationId);
bean.setPropertyValue(sourceIdPropertyName, sourceId);
}
}
}
}
private boolean shouldIgnoreDatum(T datum) {
return (datumClassNameIgnore != null && datumClassNameIgnore
.contains(datum.getClass().getName()));
}
@Override
public String toString() {
return delegate != null ? delegate.toString() + "[LocationDatumDataSource proxy]"
: "LocationDatumDataSource";
}
@Override
public String getUID() {
return delegate.getUID();
}
@Override
public String getGroupUID() {
return delegate.getGroupUID();
}
@Override
public String getSettingUID() {
if ( delegate instanceof SettingSpecifierProvider ) {
return ((SettingSpecifierProvider) delegate).getSettingUID();
}
return getClass().getName();
}
@Override
public String getDisplayName() {
if ( delegate instanceof SettingSpecifierProvider ) {
return ((SettingSpecifierProvider) delegate).getDisplayName();
}
return null;
}
@Override
public synchronized MessageSource getMessageSource() {
if ( messageSource == null ) {
MessageSource other = null;
if ( delegate instanceof SettingSpecifierProvider ) {
other = ((SettingSpecifierProvider) delegate).getMessageSource();
}
PrefixedMessageSource delegateSource = null;
if ( other != null ) {
delegateSource = new PrefixedMessageSource();
delegateSource.setDelegate(other);
delegateSource.setPrefix("delegate.");
}
ResourceBundleMessageSource proxySource = new ResourceBundleMessageSource();
proxySource.setBundleClassLoader(getClass().getClassLoader());
proxySource.setBasename(messageBundleBasename);
if ( delegateSource != null ) {
proxySource.setParentMessageSource(delegateSource);
}
messageSource = proxySource;
}
return messageSource;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
List<SettingSpecifier> result = new ArrayList<SettingSpecifier>();
result.add(getLocationSettingSpecifier());
if ( delegate instanceof SettingSpecifierProvider ) {
List<SettingSpecifier> delegateResult = ((SettingSpecifierProvider) delegate)
.getSettingSpecifiers();
if ( delegateResult != null ) {
for ( SettingSpecifier spec : delegateResult ) {
if ( spec instanceof KeyedSettingSpecifier<?> ) {
KeyedSettingSpecifier<?> keyedSpec = (KeyedSettingSpecifier<?>) spec;
result.add(keyedSpec.mappedTo("delegate."));
} else {
result.add(spec);
}
}
}
}
return result;
}
private LocationLookupSettingSpecifier getLocationSettingSpecifier() {
if ( location == null && locationService != null && locationId != null && sourceId != null ) {
LocationService service = locationService.service();
if ( service != null ) {
GeneralLocationSourceMetadata meta = service.getLocationMetadata(locationId, sourceId);
BasicGeneralLocation loc = new BasicGeneralLocation();
loc.setLocationId(locationId);
loc.setSourceId(sourceId);
loc.setSourceMetadata(meta);
location = loc;
}
}
return new BasicLocationLookupSettingSpecifier("locationKey", locationType, location);
}
public DatumDataSource<T> getDelegate() {
return delegate;
}
public void setDelegate(DatumDataSource<T> delegate) {
this.delegate = delegate;
}
public OptionalService<LocationService> getLocationService() {
return locationService;
}
public void setLocationService(OptionalService<LocationService> locationService) {
this.locationService = locationService;
}
public String getLocationIdPropertyName() {
return locationIdPropertyName;
}
public void setLocationIdPropertyName(String locationIdPropertyName) {
this.locationIdPropertyName = locationIdPropertyName;
}
public boolean isRequireLocationService() {
return requireLocationService;
}
public void setRequireLocationService(boolean requireLocationService) {
this.requireLocationService = requireLocationService;
}
public String getLocationType() {
return locationType;
}
public void setLocationType(String locationType) {
this.locationType = locationType;
}
public String getMessageBundleBasename() {
return messageBundleBasename;
}
public void setMessageBundleBasename(String messageBundleBaseName) {
this.messageBundleBasename = messageBundleBaseName;
}
/**
* Set the location ID and source ID as a single string value. The format of
* the key is {@code locationId:sourceId}.
*
* @param key
* the location and source ID key
*/
public void setLocationKey(String key) {
Long newLocationId = null;
String newSourceId = null;
if ( key != null ) {
int idx = key.indexOf(':');
if ( idx > 0 && idx + 1 < key.length() ) {
newLocationId = Long.valueOf(key.substring(0, idx));
newSourceId = key.substring(idx + 1);
}
}
setLocationId(newLocationId);
setSourceId(newSourceId);
}
public Long getLocationId() {
return locationId;
}
public void setLocationId(Long locationId) {
if ( this.location != null && locationId != null
&& !locationId.equals(this.location.getLocationId()) ) {
this.location = null; // set to null so we re-fetch from server
}
this.locationId = locationId;
}
public GeneralLocation getLocation() {
return location;
}
public Set<String> getDatumClassNameIgnore() {
return datumClassNameIgnore;
}
public void setDatumClassNameIgnore(Set<String> datumClassNameIgnore) {
this.datumClassNameIgnore = datumClassNameIgnore;
}
public String getSourceId() {
return sourceId;
}
public void setSourceId(String sourceId) {
if ( this.location != null && sourceId != null && !sourceId.equals(this.location.getSourceId()) ) {
this.location = null; // set to null so we re-fetch from server
}
this.sourceId = sourceId;
}
public String getSourceIdPropertyName() {
return sourceIdPropertyName;
}
public void setSourceIdPropertyName(String sourceIdPropertyName) {
this.sourceIdPropertyName = sourceIdPropertyName;
}
}