package org.dcache.services.billing.text;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.in;
import static com.google.common.collect.Multimaps.filterValues;
/**
* Builder for creating parsers for billing file entries.
*/
public class BillingParserBuilder
{
private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("\\$(.+?)\\$");
private ImmutableSetMultimap<String, Pattern> patternsByAttribute;
private ImmutableSetMultimap<Pattern, String> attributesByPattern;
private Map<String, String> formats;
private final Set<String> attributes = new LinkedHashSet<>();
private boolean canOutputArray = true;
public BillingParserBuilder(Map<String,String> formats)
{
this.formats = Maps.newHashMap(formats);
attributesByPattern = toPatterns(formats);
patternsByAttribute = attributesByPattern.inverse();
}
public BillingParserBuilder addAttribute(String attribute)
{
attributes.add(attribute);
return this;
}
public BillingParserBuilder addAllAttributes()
{
attributes.clear();
attributes.addAll(patternsByAttribute.keySet());
canOutputArray = false;
return this;
}
public BillingParserBuilder withFormat(String message, String format)
{
formats.put(message, format);
attributesByPattern = toPatterns(formats);
patternsByAttribute = attributesByPattern.inverse();
return this;
}
public BillingParserBuilder withFormat(String header)
{
String[] s = header.substring(2).trim().split(" ", 2);
return (s.length == 2) ? withFormat(s[0], s[1]) : this;
}
public Function<String,String> buildToString()
{
String attribute = Iterables.getOnlyElement(attributes);
String groupName = toGroupName(attribute);
ImmutableSet<Pattern> patterns = patternsByAttribute.get(attribute);
return line -> findSingleMatch(line, patterns, groupName);
}
public Function<String,Map<String,String>> buildToMap()
{
ImmutableMultimap<Pattern, String> patterns =
ImmutableMultimap.copyOf(filterValues(attributesByPattern, in(attributes)));
return line -> findMatchAsMap(line, patterns);
}
public Function<String,String[]> buildToArray()
{
checkState(canOutputArray);
final ImmutableMultimap<Pattern, String> patterns =
ImmutableMultimap.copyOf(filterValues(attributesByPattern, in(this.attributes)));
final String[] attributes = this.attributes.toArray(new String[this.attributes.size()]);
return line -> findMatchAsArray(line, patterns, attributes);
}
private static String findSingleMatch(String line, ImmutableSet<Pattern> patterns, String groupName)
{
Matcher matcher = findMatch(line, patterns);
return matcher != null ? matcher.group(groupName) : null;
}
private static Map<String, String> findMatchAsMap(String line, ImmutableMultimap<Pattern, String> patterns)
{
Matcher matcher = findMatch(line, patterns.keySet());
if (matcher == null) {
return Collections.emptyMap();
}
Map<String, String> values = new HashMap<>();
for (String attribute : patterns.get(matcher.pattern())) {
values.put(attribute, matcher.group(toGroupName(attribute)));
}
return values;
}
private static String[] findMatchAsArray(String line, ImmutableMultimap<Pattern, String> patterns,
String[] attributes)
{
Matcher matcher = findMatch(line, patterns.keySet());
String[] result = new String[attributes.length];
if (matcher != null) {
ImmutableCollection<String> attributesInPattern = patterns.get(matcher.pattern());
for (int i = 0; i < attributes.length; i++) {
String attribute = attributes[i];
if (attributesInPattern.contains(attribute)) {
result[i] = matcher.group(toGroupName(attribute));
}
}
}
return result;
}
private static Matcher findMatch(String line, Collection<Pattern> patterns)
{
Matcher result = null;
for (Pattern pattern : patterns) {
Matcher matcher = pattern.matcher(line);
if (matcher.matches()) {
if (result != null) {
throw new IllegalArgumentException("Duplicate matches for: " + line);
}
result = matcher;
}
}
return result;
}
/**
* Returns Patterns for the provided billing formats, as a Multimap mapping the
* Pattern to the attributes contained in the pattern.
*/
private static ImmutableSetMultimap<Pattern, String> toPatterns(Map<String,String> formats)
{
ImmutableSetMultimap.Builder<Pattern, String> builder = ImmutableSetMultimap.builder();
for (Map.Entry<String, String> format: formats.entrySet()) {
builder.putAll(toPattern(format.getKey(), format.getValue()), toAttributes(format.getValue()));
}
return builder.build();
}
/**
* Returns a Pattern for matching the provided billing format.
*
* Attributes are turned into named capturing groups.
*/
private static Pattern toPattern(String name, String format)
{
StringBuilder regex = new StringBuilder();
Matcher matcher = ATTRIBUTE_PATTERN.matcher(format);
int pos = 0;
while (matcher.find()) {
if (pos < matcher.start()) {
regex.append(Pattern.quote(format.substring(pos, matcher.start())));
}
String expression = matcher.group(1);
if (isIf(expression)) {
regex.append("(?:");
} else if (isElse(expression)) {
regex.append("|");
} else if (isEndIf(expression)) {
regex.append(")");
} else {
regex.append("(?<").append(toGroupName(expression)).append(">");
// This incomplete list of attribute patterns reduces the risk of false matches
switch (expression) {
case "date":
regex.append(".+?");
break;
case "pnfsid":
regex.append("[0-9A-F]{24}(?:[0-9A-F]{12})?");
break;
case "filesize":
case "transferred":
case "connectionTime":
case "transactionTime":
case "queuingTime":
case "transferTime":
case "rc":
case "uid":
case "gid":
regex.append("-?\\d+");
break;
case "cached":
case "created":
regex.append("(?:true|false)");
break;
case "cellType":
switch (name) {
case "mover-info-message":
case "remove-file-info-message":
case "storage-info-message":
case "pool-hit-info-message":
regex.append("pool");
break;
case "door-request-info-message":
regex.append("door");
break;
default:
regex.append("\\w+");
break;
}
break;
case "cellName":
regex.append(".+?");
break;
case "type":
switch (name) {
case "mover-info-message":
regex.append("transfer");
break;
case "remove-file-info-message":
regex.append("remove");
break;
case "storage-info-message":
regex.append("(?:re)?store");
break;
case "pool-hit-info-message":
regex.append("hit");
break;
case "warning-pnfs-file-info-message":
regex.append("warning");
break;
default:
regex.append("\\w+");
break;
}
break;
default:
regex.append(".*?");
}
regex.append(")");
}
pos = matcher.end();
}
if (pos < format.length()) {
regex.append(Pattern.quote(format.substring(pos)));
}
return Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE);
}
/**
* Translates a attribute name into a name suitable for a named capturing group.
*/
private static String toGroupName(String attribute)
{
int pos = attribute.indexOf(';');
if (pos > -1) {
attribute = attribute.substring(0, pos);
}
return attribute.replace("X", "XX").replace(".", "X");
}
/**
* Returns names of all attributes in the provided billing format.
*/
private static Set<String> toAttributes(String format)
{
Set<String> attributes = new HashSet<>();
Matcher matcher = ATTRIBUTE_PATTERN.matcher(format);
while (matcher.find()) {
String expression = matcher.group(1);
if (!isIf(expression) && !isElse(expression) && !isEndIf(expression)) {
int pos = expression.indexOf(';');
attributes.add(pos > -1 ? expression.substring(0, pos) : expression);
}
}
return attributes;
}
/**
* True if the given string template expression is the beginning of an if-expression.
*/
private static boolean isIf(String expression)
{
return expression.startsWith("if(") && expression.endsWith(")");
}
/**
* True if the given string template expression is the else keyword of an if-expression.
*/
private static boolean isElse(String expression)
{
return expression.equals("else");
}
/**
* True if the given string template expression is the end of an if-expression.
*/
private static boolean isEndIf(String expression)
{
return expression.equals("endif");
}
}