/* ==================================================================
* SimpleFilterSampleTransformer.java - 28/10/2016 3:00:56 PM
*
* Copyright 2007-2016 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.datum.samplefilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import net.solarnetwork.domain.GeneralDatumSamples;
import net.solarnetwork.node.domain.Datum;
import net.solarnetwork.node.domain.GeneralDatumSamplesTransformer;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier;
import net.solarnetwork.node.settings.support.SettingsUtil;
/**
* {@link GeneralDatumSamplesTransformer} that can filter out sample properties
* based on simple matching rules.
*
* @author matt
* @version 1.0
*/
public class SimpleFilterSampleTransformer
implements GeneralDatumSamplesTransformer, SettingSpecifierProvider {
private Pattern sourceId;
private String[] includes;
private String[] excludes;
private MessageSource messageSource;
private Pattern[] includePatterns;
private Pattern[] excludePatterns;
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
public GeneralDatumSamples transformSamples(Datum datum, GeneralDatumSamples samples) {
Pattern sourceIdPat = sourceId;
if ( sourceIdPat != null ) {
if ( datum == null || datum.getSourceId() == null
|| !sourceIdPat.matcher(datum.getSourceId()).find() ) {
log.trace("Datum {} does not match source ID pattern {}; not filtering", datum,
sourceId);
return samples;
}
}
GeneralDatumSamples copy = null;
// handle property inclusion rules
Pattern[] incs = this.includePatterns;
if ( incs != null && incs.length > 0 ) {
Map<String, ?> map = samples.getAccumulating();
if ( map != null ) {
for ( String propName : map.keySet() ) {
if ( !matchesAny(incs, propName, true) ) {
if ( copy == null ) {
copy = copy(samples);
}
copy.getAccumulating().remove(propName);
}
}
}
map = samples.getInstantaneous();
if ( map != null ) {
for ( String propName : map.keySet() ) {
if ( !matchesAny(incs, propName, true) ) {
if ( copy == null ) {
copy = copy(samples);
}
copy.getInstantaneous().remove(propName);
}
}
}
map = samples.getStatus();
if ( map != null ) {
for ( String propName : map.keySet() ) {
if ( !matchesAny(incs, propName, true) ) {
if ( copy == null ) {
copy = copy(samples);
}
copy.getStatus().remove(propName);
}
}
}
}
// handle property exclusion rules
Pattern[] excs = this.excludePatterns;
if ( excs != null && excs.length > 0 ) {
Map<String, ?> map = samples.getAccumulating();
if ( map != null ) {
for ( String propName : map.keySet() ) {
if ( matchesAny(excs, propName, false) ) {
if ( copy == null ) {
copy = copy(samples);
}
copy.getAccumulating().remove(propName);
}
}
}
map = samples.getInstantaneous();
if ( map != null ) {
for ( String propName : map.keySet() ) {
if ( matchesAny(excs, propName, false) ) {
if ( copy == null ) {
copy = copy(samples);
}
copy.getInstantaneous().remove(propName);
}
}
}
map = samples.getStatus();
if ( map != null ) {
for ( String propName : map.keySet() ) {
if ( matchesAny(excs, propName, false) ) {
if ( copy == null ) {
copy = copy(samples);
}
copy.getStatus().remove(propName);
}
}
}
}
// tidy up any empty maps we created during filtering
if ( copy != null )
{
if ( copy.getAccumulating() != null && copy.getAccumulating().isEmpty() ) {
copy.setAccumulating(null);
}
if ( copy.getInstantaneous() != null && copy.getInstantaneous().isEmpty() ) {
copy.setInstantaneous(null);
}
if ( copy.getStatus() != null && copy.getStatus().isEmpty() ) {
copy.setStatus(null);
}
}
return (copy != null ? copy : samples);
}
private boolean matchesAny(final Pattern[] pats, final String value,
final boolean emptyPatternMatches) {
if ( pats == null || pats.length < 1 || value == null ) {
return true;
}
for ( Pattern pat : pats ) {
if ( pat == null ) {
if ( emptyPatternMatches ) {
return true;
}
continue;
}
if ( pat.matcher(value).find() ) {
return true;
}
}
return false;
}
private static GeneralDatumSamples copy(GeneralDatumSamples samples) {
GeneralDatumSamples copy = new GeneralDatumSamples(
samples.getInstantaneous() != null
? new LinkedHashMap<String, Number>(samples.getInstantaneous()) : null,
samples.getAccumulating() != null
? new LinkedHashMap<String, Number>(samples.getAccumulating()) : null,
samples.getStatus() != null ? new LinkedHashMap<String, Object>(samples.getStatus())
: null);
copy.setTags(samples.getTags() != null ? new LinkedHashSet<String>(samples.getTags()) : null);
return copy;
}
/**
* Call to initialize the instance after properties are configured.
*/
public void init() {
configurationChanged(null);
}
/**
* Call after any of the include/exclude values are modified.
*/
public void configurationChanged(Map<String, ?> props) {
this.includePatterns = patterns(getIncludes());
this.excludePatterns = patterns(getExcludes());
}
@Override
public String getSettingUID() {
return "net.solarnetwork.node.datum.samplefilter.simple";
}
@Override
public String getDisplayName() {
return "Simple Filter Sample Transformer";
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(3);
results.add(new BasicTextFieldSettingSpecifier("sourceId", ""));
String[] incs = getIncludes();
Collection<String> listStrings = (incs != null ? Arrays.asList(incs)
: Collections.<String> emptyList());
results.add(SettingsUtil.dynamicListSettingSpecifier("includes", listStrings,
new SettingsUtil.KeyedListCallback<String>() {
@Override
public Collection<SettingSpecifier> mapListSettingKey(String value, int index,
String key) {
return Collections.<SettingSpecifier> singletonList(
new BasicTextFieldSettingSpecifier(key, ""));
}
}));
String[] excs = getExcludes();
listStrings = (excs != null ? Arrays.asList(excs) : Collections.<String> emptyList());
results.add(SettingsUtil.dynamicListSettingSpecifier("excludes", listStrings,
new SettingsUtil.KeyedListCallback<String>() {
@Override
public Collection<SettingSpecifier> mapListSettingKey(String value, int index,
String key) {
return Collections.<SettingSpecifier> singletonList(
new BasicTextFieldSettingSpecifier(key, ""));
}
}));
return results;
}
private Pattern[] patterns(String[] expressions) {
Pattern[] pats = null;
if ( expressions != null ) {
final int len = expressions.length;
pats = new Pattern[len];
for ( int i = 0; i < len; i++ ) {
if ( expressions[i] == null || expressions[i].length() < 1 ) {
continue;
}
try {
pats[i] = Pattern.compile(expressions[i], Pattern.CASE_INSENSITIVE);
} catch ( PatternSyntaxException e ) {
log.warn("Error compiling includePatterns regex [{}]", expressions[i], e);
}
}
}
return pats;
}
/**
* Get the source ID pattern.
*
* @return The pattern.
*/
public String getSourceId() {
return (sourceId != null ? sourceId.pattern() : null);
}
/**
* Set a source ID pattern to match samples against.
*
* Samples will only be considered for filtering if
* {@link Datum#getSourceId()} matches this pattern.
*
* The {@code sourceIdPattern} must be a valid {@link Pattern} regular
* expression. The expression will be allowed to match anywhere in
* {@link Datum#getSourceId()} values, so if the pattern must match the full
* value only then use pattern positional expressions like {@code ^} and
* {@code $}.
*
* @param sourceIdPattern
* The source ID regex to match. Syntax errors in the pattern will be
* ignored and a {@code null} value will be set instead.
*/
public void setSourceId(String sourceIdPattern) {
try {
this.sourceId = (sourceIdPattern != null
? Pattern.compile(sourceIdPattern, Pattern.CASE_INSENSITIVE) : null);
} catch ( PatternSyntaxException e ) {
log.warn("Error compiling regex [{}]", sourceIdPattern, e);
this.sourceId = null;
}
}
/**
* Get the property include expressions.
*
* @return The property include expressions.
*/
public String[] getIncludes() {
return this.includes;
}
/**
* Set an array of property include expressions.
*
* @param includeExpressions
* The property include expressions.
*/
public void setIncludes(String[] includeExpressions) {
this.includes = includeExpressions;
}
/**
* Get the number of configured {@code includes} elements.
*
* @return The number of {@code includes} elements.
*/
public int getIncludesCount() {
String[] pats = this.includes;
return (pats == null ? 0 : pats.length);
}
/**
* Adjust the number of configured {@code includes} elements. Any newly
* added element values will be {@code null}.
*
* @param count
* The desired number of {@code includes} elements.
*/
public void setIncludesCount(int count) {
if ( count < 0 ) {
count = 0;
}
String[] pats = this.includes;
int lCount = (pats == null ? 0 : pats.length);
if ( lCount != count ) {
String[] newPats = new String[count];
if ( pats != null ) {
System.arraycopy(pats, 0, newPats, 0, Math.min(count, pats.length));
}
this.includes = newPats;
}
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
/**
* Set a {@link MessageSource} to use for settings.
*
* @param messageSource
* The message source.
*/
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* Get the array of property exclude expressions.
*
* @return The property exclude expressions.
*/
public String[] getExcludes() {
return excludes;
}
/**
* Set an array of property exclude expressions.
*
* @param excludeExpressions
* The property patterns to exclude.
*/
public void setExcludes(String[] excludeExpressions) {
this.excludes = excludeExpressions;
}
/**
* Get the number of configured {@code excludes} elements.
*
* @return The number of {@code excludes} elements.
*/
public int getExcludesCount() {
String[] pats = this.excludes;
return (pats == null ? 0 : pats.length);
}
/**
* Adjust the number of configured {@code excludes} elements. Any newly
* added element values will be {@code null}.
*
* @param count
* The desired number of {@code excludes} elements.
*/
public void setExcludesCount(int count) {
if ( count < 0 ) {
count = 0;
}
String[] pats = this.excludes;
int lCount = (pats == null ? 0 : pats.length);
if ( lCount != count ) {
String[] newPats = new String[count];
if ( pats != null ) {
System.arraycopy(pats, 0, newPats, 0, Math.min(count, pats.length));
}
this.excludes = newPats;
}
}
}