blob: 71a423ac91d961664a8ef95a2d56cdea07e3e1b2 [file] [log] [blame]
Simon Hunte556e942017-06-19 15:35:44 -07001/*
2 * Copyright 2017-present Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18package org.onosproject.ui.lion.stitch;
19
20import com.google.common.collect.ImmutableList;
21import com.google.common.collect.ImmutableSortedSet;
22import org.apache.commons.io.IOUtils;
23
24import java.io.IOException;
25import java.io.InputStream;
26import java.util.ArrayList;
27import java.util.Collections;
28import java.util.HashMap;
29import java.util.HashSet;
30import java.util.List;
31import java.util.Map;
32import java.util.Set;
33import java.util.TreeSet;
34import java.util.regex.Matcher;
35import java.util.regex.Pattern;
36
37import static java.nio.charset.StandardCharsets.UTF_8;
38
39/**
40 * A Java representation of a lion configuration file. You can create one with
41 * something like the following:
42 * <pre>
43 * String filepath = "/path/to/some/file.lioncfg";
44 * LionConfig cfg = new LionConfig().load(filepath);
45 * </pre>
46 */
47public class LionConfig {
48 private static final Pattern RE_COMMENT = Pattern.compile("^\\s*#.*");
49 private static final Pattern RE_BLANK = Pattern.compile("^\\s*$");
50
51 static final Pattern RE_IMPORT =
52 Pattern.compile("^(\\S+)\\s+import\\s+(.*)$");
53
54 private static final String BUNDLE = "bundle";
55 private static final String ALIAS = "alias";
56 private static final String FROM = "from";
57 private static final String STAR = "*";
58 private static final char SPC = ' ';
59 private static final char DOT = '.';
60
61 private List<String> lines;
62 private List<String> badLines;
63
64 private CmdBundle bundle;
65 private final Set<CmdAlias> aliases = new TreeSet<>();
66 private final Set<CmdFrom> froms = new TreeSet<>();
67
68 private Map<String, String> aliasMap;
69 private Map<String, Set<String>> fromMap;
70
71 /**
72 * Loads in the specified file and attempts to parse it as a
73 * {@code .lioncfg} format file.
74 *
75 * @param source path to .lioncfg file
76 * @return the instance
77 * @throws IllegalArgumentException if there is a problem reading the file
78 */
79 public LionConfig load(String source) {
80 InputStream is = getClass().getResourceAsStream(source);
81 try {
82 lines = IOUtils.readLines(is, UTF_8);
Simon Hunt7379a3d2017-06-20 16:50:39 -070083 } catch (NullPointerException | IOException e) {
Simon Hunte556e942017-06-19 15:35:44 -070084 throw new IllegalArgumentException("Failed to read: " + source, e);
85 }
86
87 stripCommentsAndWhitespace();
88 parse();
89 processAliases();
90 processFroms();
91
92 return this;
93 }
94
95
96 private boolean isCommentOrBlank(String s) {
97 return RE_COMMENT.matcher(s).matches() || RE_BLANK.matcher(s).matches();
98 }
99
100
101 private void stripCommentsAndWhitespace() {
102 if (lines != null) {
103 lines.removeIf(this::isCommentOrBlank);
104 }
105 }
106
107 private void parse() {
108 badLines = new ArrayList<>();
109
110 lines.forEach(l -> {
111 int i = l.indexOf(SPC);
112 if (i < 1) {
113 badLines.add(l);
114 return;
115 }
116 String keyword = l.substring(0, i);
117 String params = l.substring(i + 1);
118
119 switch (keyword) {
120 case BUNDLE:
121 CmdBundle cb = new CmdBundle(l, params);
122
123 if (bundle != null) {
124 // we can only declare the bundle once
125 badLines.add(l);
126 } else {
127 bundle = cb;
128 }
129 break;
130
131 case ALIAS:
132 CmdAlias ca = new CmdAlias(l, params);
133 if (ca.malformed) {
134 badLines.add(l);
135 } else {
136 aliases.add(ca);
137 }
138 break;
139
140 case FROM:
141 CmdFrom cf = new CmdFrom(l, params);
142 if (cf.malformed) {
143 badLines.add(l);
144 } else {
145 froms.add(cf);
146 }
147 break;
148
149 default:
150 badLines.add(l);
151 break;
152 }
153 });
154 }
155
156 private void processAliases() {
157 aliasMap = new HashMap<>(aliasCount());
158 aliases.forEach(a -> aliasMap.put(a.alias, a.subst));
159 }
160
161 private void processFroms() {
162 fromMap = new HashMap<>(fromCount());
163 froms.forEach(f -> {
164 f.expandAliasIfAny(aliasMap);
165 if (singleStarCheck(f)) {
166 fromMap.put(f.expandedRes, f.keys);
167 } else {
168 badLines.add(f.orig);
169 }
170 });
171 }
172
173 private boolean singleStarCheck(CmdFrom from) {
174 from.starred = false;
175 Set<String> keys = from.keys();
Simon Hunt7379a3d2017-06-20 16:50:39 -0700176 for (String k : keys) {
Simon Hunte556e942017-06-19 15:35:44 -0700177 if (STAR.equals(k)) {
178 from.starred = true;
179 }
180 }
181 return !from.starred || keys.size() == 1;
182 }
183
184 @Override
185 public String toString() {
186 int nlines = lines == null ? 0 : lines.size();
187 return String.format("LionConfig{#lines=%d}", nlines);
188 }
189
190 /**
191 * Returns the configured bundle ID for this config.
192 *
193 * @return the bundle ID
194 */
195 String id() {
196 return bundle == null ? null : bundle.id;
197 }
198
199 /**
200 * Returns the number of aliases configured in this config.
201 *
202 * @return the alias count
203 */
204 int aliasCount() {
205 return aliases.size();
206 }
207
208 /**
209 * Returns the number of from...import lines configured in this config.
210 *
211 * @return the number of from...import lines
212 */
213 int fromCount() {
214 return froms.size();
215 }
216
217 /**
218 * Returns the substitution string for the given alias.
219 *
220 * @param a the alias
221 * @return the substitution
222 */
223 String alias(String a) {
224 return aliasMap.get(a);
225 }
226
227 /**
228 * Returns the number of keys imported from the specified resource.
229 *
230 * @param res the resource
231 * @return number of keys imported from that resource
232 */
233 int fromKeyCount(String res) {
234 Set<String> keys = fromMap.get(res);
235 return keys == null ? 0 : keys.size();
236 }
237
238 /**
239 * Returns true if the specified resource exists and contains the
240 * given key.
241 *
242 * @param res the resource
243 * @param key the key
244 * @return true, if resource exists and contains the key; false otherwise
245 */
246 boolean fromContains(String res, String key) {
247 Set<String> keys = fromMap.get(res);
248 return keys != null && keys.contains(key);
249 }
250
251 /**
252 * Returns the set of (expanded) "from" entries in this configuration.
253 *
254 * @return the entries
255 */
256 public Set<CmdFrom> entries() {
257 return froms;
258 }
259
260 /**
261 * Returns the number of parse errors detected.
262 *
263 * @return number of bad lines
264 */
265 public int errorCount() {
266 return badLines.size();
267 }
268
269 /**
270 * Returns the lines that failed the parser.
271 *
272 * @return the erroneous lines in the config
273 */
274 public List<String> errorLines() {
275 return ImmutableList.copyOf(badLines);
276 }
277
278 // ==== Mini class hierarchy of command types
279
280 private abstract static class Cmd {
281 final String orig;
282 boolean malformed = false;
283
284 Cmd(String orig) {
285 this.orig = orig;
286 }
287 }
288
289 private static final class CmdBundle extends Cmd {
290 private final String id;
291
292 private CmdBundle(String orig, String params) {
293 super(orig);
294 id = params;
295 }
296
297 @Override
298 public String toString() {
299 return "CmdBundle{id=\"" + id + "\"}";
300 }
301 }
302
303 private static final class CmdAlias extends Cmd
304 implements Comparable<CmdAlias> {
305 private final String alias;
306 private final String subst;
307
308 private CmdAlias(String orig, String params) {
309 super(orig);
310 int i = params.indexOf(SPC);
311 if (i < 1) {
312 malformed = true;
313 alias = null;
314 subst = null;
315 } else {
316 alias = params.substring(0, i);
317 subst = params.substring(i + 1);
318 }
319 }
320
321 @Override
322 public String toString() {
323 return "CmdAlias{alias=\"" + alias + "\", subst=\"" + subst + "\"}";
324 }
325
326 @Override
327 public int compareTo(CmdAlias o) {
328 return alias.compareTo(o.alias);
329 }
330 }
331
332 /**
333 * Represents a "from {res} import {stuff}" command in the configuration.
334 */
335 public static final class CmdFrom extends Cmd
336 implements Comparable<CmdFrom> {
337 private final String rawRes;
338 private final Set<String> keys;
339 private String expandedRes;
340 private boolean starred = false;
341
342 private CmdFrom(String orig, String params) {
343 super(orig);
344 Matcher m = RE_IMPORT.matcher(params);
345 if (!m.matches()) {
346 malformed = true;
347 rawRes = null;
348 keys = null;
349 } else {
350 rawRes = m.group(1);
351 keys = genKeys(m.group(2));
352 }
353 }
354
355 private Set<String> genKeys(String keys) {
356 String[] k = keys.split("\\s*,\\s*");
357 Set<String> allKeys = new HashSet<>();
358 Collections.addAll(allKeys, k);
359 return ImmutableSortedSet.copyOf(allKeys);
360 }
361
362 private void expandAliasIfAny(Map<String, String> aliases) {
363 String expanded = rawRes;
364 int i = rawRes.indexOf(DOT);
365 if (i > 0) {
366 String alias = rawRes.substring(0, i);
367 String sub = aliases.get(alias);
368 if (sub != null) {
369 expanded = sub + rawRes.substring(i);
370 }
371 }
372 expandedRes = expanded;
373 }
374
375 @Override
376 public String toString() {
377 return "CmdFrom{res=\"" + rawRes + "\", keys=" + keys + "}";
378 }
379
380 @Override
381 public int compareTo(CmdFrom o) {
382 return rawRes.compareTo(o.rawRes);
383 }
384
385 /**
386 * Returns the resource bundle name from which to import things.
387 *
388 * @return the resource bundle name
389 */
390 public String res() {
391 return expandedRes;
392 }
393
394 /**
395 * Returns the set of keys which should be imported.
396 *
397 * @return the keys to import
398 */
399 public Set<String> keys() {
400 return keys;
401 }
402
403 /**
404 * Returns true if this "from" command is importing ALL keys from
405 * the specified resource; false otherwise.
406 *
407 * @return true, if importing ALL keys; false otherwise
408 */
409 public boolean starred() {
410 return starred;
411 }
412 }
413}