blob: 71a423ac91d961664a8ef95a2d56cdea07e3e1b2 [file] [log] [blame]
/*
* Copyright 2017-present Open Networking Laboratory
*
* Licensed 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.onosproject.ui.lion.stitch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* A Java representation of a lion configuration file. You can create one with
* something like the following:
* <pre>
* String filepath = "/path/to/some/file.lioncfg";
* LionConfig cfg = new LionConfig().load(filepath);
* </pre>
*/
public class LionConfig {
private static final Pattern RE_COMMENT = Pattern.compile("^\\s*#.*");
private static final Pattern RE_BLANK = Pattern.compile("^\\s*$");
static final Pattern RE_IMPORT =
Pattern.compile("^(\\S+)\\s+import\\s+(.*)$");
private static final String BUNDLE = "bundle";
private static final String ALIAS = "alias";
private static final String FROM = "from";
private static final String STAR = "*";
private static final char SPC = ' ';
private static final char DOT = '.';
private List<String> lines;
private List<String> badLines;
private CmdBundle bundle;
private final Set<CmdAlias> aliases = new TreeSet<>();
private final Set<CmdFrom> froms = new TreeSet<>();
private Map<String, String> aliasMap;
private Map<String, Set<String>> fromMap;
/**
* Loads in the specified file and attempts to parse it as a
* {@code .lioncfg} format file.
*
* @param source path to .lioncfg file
* @return the instance
* @throws IllegalArgumentException if there is a problem reading the file
*/
public LionConfig load(String source) {
InputStream is = getClass().getResourceAsStream(source);
try {
lines = IOUtils.readLines(is, UTF_8);
} catch (NullPointerException | IOException e) {
throw new IllegalArgumentException("Failed to read: " + source, e);
}
stripCommentsAndWhitespace();
parse();
processAliases();
processFroms();
return this;
}
private boolean isCommentOrBlank(String s) {
return RE_COMMENT.matcher(s).matches() || RE_BLANK.matcher(s).matches();
}
private void stripCommentsAndWhitespace() {
if (lines != null) {
lines.removeIf(this::isCommentOrBlank);
}
}
private void parse() {
badLines = new ArrayList<>();
lines.forEach(l -> {
int i = l.indexOf(SPC);
if (i < 1) {
badLines.add(l);
return;
}
String keyword = l.substring(0, i);
String params = l.substring(i + 1);
switch (keyword) {
case BUNDLE:
CmdBundle cb = new CmdBundle(l, params);
if (bundle != null) {
// we can only declare the bundle once
badLines.add(l);
} else {
bundle = cb;
}
break;
case ALIAS:
CmdAlias ca = new CmdAlias(l, params);
if (ca.malformed) {
badLines.add(l);
} else {
aliases.add(ca);
}
break;
case FROM:
CmdFrom cf = new CmdFrom(l, params);
if (cf.malformed) {
badLines.add(l);
} else {
froms.add(cf);
}
break;
default:
badLines.add(l);
break;
}
});
}
private void processAliases() {
aliasMap = new HashMap<>(aliasCount());
aliases.forEach(a -> aliasMap.put(a.alias, a.subst));
}
private void processFroms() {
fromMap = new HashMap<>(fromCount());
froms.forEach(f -> {
f.expandAliasIfAny(aliasMap);
if (singleStarCheck(f)) {
fromMap.put(f.expandedRes, f.keys);
} else {
badLines.add(f.orig);
}
});
}
private boolean singleStarCheck(CmdFrom from) {
from.starred = false;
Set<String> keys = from.keys();
for (String k : keys) {
if (STAR.equals(k)) {
from.starred = true;
}
}
return !from.starred || keys.size() == 1;
}
@Override
public String toString() {
int nlines = lines == null ? 0 : lines.size();
return String.format("LionConfig{#lines=%d}", nlines);
}
/**
* Returns the configured bundle ID for this config.
*
* @return the bundle ID
*/
String id() {
return bundle == null ? null : bundle.id;
}
/**
* Returns the number of aliases configured in this config.
*
* @return the alias count
*/
int aliasCount() {
return aliases.size();
}
/**
* Returns the number of from...import lines configured in this config.
*
* @return the number of from...import lines
*/
int fromCount() {
return froms.size();
}
/**
* Returns the substitution string for the given alias.
*
* @param a the alias
* @return the substitution
*/
String alias(String a) {
return aliasMap.get(a);
}
/**
* Returns the number of keys imported from the specified resource.
*
* @param res the resource
* @return number of keys imported from that resource
*/
int fromKeyCount(String res) {
Set<String> keys = fromMap.get(res);
return keys == null ? 0 : keys.size();
}
/**
* Returns true if the specified resource exists and contains the
* given key.
*
* @param res the resource
* @param key the key
* @return true, if resource exists and contains the key; false otherwise
*/
boolean fromContains(String res, String key) {
Set<String> keys = fromMap.get(res);
return keys != null && keys.contains(key);
}
/**
* Returns the set of (expanded) "from" entries in this configuration.
*
* @return the entries
*/
public Set<CmdFrom> entries() {
return froms;
}
/**
* Returns the number of parse errors detected.
*
* @return number of bad lines
*/
public int errorCount() {
return badLines.size();
}
/**
* Returns the lines that failed the parser.
*
* @return the erroneous lines in the config
*/
public List<String> errorLines() {
return ImmutableList.copyOf(badLines);
}
// ==== Mini class hierarchy of command types
private abstract static class Cmd {
final String orig;
boolean malformed = false;
Cmd(String orig) {
this.orig = orig;
}
}
private static final class CmdBundle extends Cmd {
private final String id;
private CmdBundle(String orig, String params) {
super(orig);
id = params;
}
@Override
public String toString() {
return "CmdBundle{id=\"" + id + "\"}";
}
}
private static final class CmdAlias extends Cmd
implements Comparable<CmdAlias> {
private final String alias;
private final String subst;
private CmdAlias(String orig, String params) {
super(orig);
int i = params.indexOf(SPC);
if (i < 1) {
malformed = true;
alias = null;
subst = null;
} else {
alias = params.substring(0, i);
subst = params.substring(i + 1);
}
}
@Override
public String toString() {
return "CmdAlias{alias=\"" + alias + "\", subst=\"" + subst + "\"}";
}
@Override
public int compareTo(CmdAlias o) {
return alias.compareTo(o.alias);
}
}
/**
* Represents a "from {res} import {stuff}" command in the configuration.
*/
public static final class CmdFrom extends Cmd
implements Comparable<CmdFrom> {
private final String rawRes;
private final Set<String> keys;
private String expandedRes;
private boolean starred = false;
private CmdFrom(String orig, String params) {
super(orig);
Matcher m = RE_IMPORT.matcher(params);
if (!m.matches()) {
malformed = true;
rawRes = null;
keys = null;
} else {
rawRes = m.group(1);
keys = genKeys(m.group(2));
}
}
private Set<String> genKeys(String keys) {
String[] k = keys.split("\\s*,\\s*");
Set<String> allKeys = new HashSet<>();
Collections.addAll(allKeys, k);
return ImmutableSortedSet.copyOf(allKeys);
}
private void expandAliasIfAny(Map<String, String> aliases) {
String expanded = rawRes;
int i = rawRes.indexOf(DOT);
if (i > 0) {
String alias = rawRes.substring(0, i);
String sub = aliases.get(alias);
if (sub != null) {
expanded = sub + rawRes.substring(i);
}
}
expandedRes = expanded;
}
@Override
public String toString() {
return "CmdFrom{res=\"" + rawRes + "\", keys=" + keys + "}";
}
@Override
public int compareTo(CmdFrom o) {
return rawRes.compareTo(o.rawRes);
}
/**
* Returns the resource bundle name from which to import things.
*
* @return the resource bundle name
*/
public String res() {
return expandedRes;
}
/**
* Returns the set of keys which should be imported.
*
* @return the keys to import
*/
public Set<String> keys() {
return keys;
}
/**
* Returns true if this "from" command is importing ALL keys from
* the specified resource; false otherwise.
*
* @return true, if importing ALL keys; false otherwise
*/
public boolean starred() {
return starred;
}
}
}