• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yahoo / elide / #7458

19 Nov 2025 02:09PM UTC coverage: 84.417%. Remained the same
#7458

push

justin-tay
Fix spring transactional resource leak

3 of 3 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

19741 of 23385 relevant lines covered (84.42%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.47
/elide-model-config/src/main/java/com/yahoo/elide/modelconfig/validator/DynamicConfigValidator.java
1
/*
2
 * Copyright 2020, Yahoo Inc.
3
 * Licensed under the Apache License, Version 2.0
4
 * See LICENSE file in project root for terms.
5
 */
6
package com.yahoo.elide.modelconfig.validator;
7

8
import static com.yahoo.elide.core.dictionary.EntityDictionary.NO_VERSION;
9
import static org.apache.commons.lang3.StringUtils.isBlank;
10
import static org.apache.commons.lang3.StringUtils.isNotBlank;
11

12
import com.yahoo.elide.annotation.Include;
13
import com.yahoo.elide.annotation.SecurityCheck;
14
import com.yahoo.elide.core.dictionary.EntityDictionary;
15
import com.yahoo.elide.core.dictionary.EntityDictionaryBuilderCustomizer;
16
import com.yahoo.elide.core.dictionary.EntityPermissions;
17
import com.yahoo.elide.core.exceptions.BadRequestException;
18
import com.yahoo.elide.core.security.checks.Check;
19
import com.yahoo.elide.core.security.checks.FilterExpressionCheck;
20
import com.yahoo.elide.core.security.checks.UserCheck;
21
import com.yahoo.elide.core.type.Type;
22
import com.yahoo.elide.core.utils.ClassScanner;
23
import com.yahoo.elide.core.utils.DefaultClassScanner;
24
import com.yahoo.elide.modelconfig.Config;
25
import com.yahoo.elide.modelconfig.DynamicConfigHelpers;
26
import com.yahoo.elide.modelconfig.DynamicConfigSchemaValidator;
27
import com.yahoo.elide.modelconfig.DynamicConfiguration;
28
import com.yahoo.elide.modelconfig.io.FileLoader;
29
import com.yahoo.elide.modelconfig.model.Argument;
30
import com.yahoo.elide.modelconfig.model.DBConfig;
31
import com.yahoo.elide.modelconfig.model.Dimension;
32
import com.yahoo.elide.modelconfig.model.ElideDBConfig;
33
import com.yahoo.elide.modelconfig.model.ElideNamespaceConfig;
34
import com.yahoo.elide.modelconfig.model.ElideSQLDBConfig;
35
import com.yahoo.elide.modelconfig.model.ElideSecurityConfig;
36
import com.yahoo.elide.modelconfig.model.ElideTableConfig;
37
import com.yahoo.elide.modelconfig.model.Join;
38
import com.yahoo.elide.modelconfig.model.Measure;
39
import com.yahoo.elide.modelconfig.model.Named;
40
import com.yahoo.elide.modelconfig.model.NamespaceConfig;
41
import com.yahoo.elide.modelconfig.model.Table;
42
import com.yahoo.elide.modelconfig.model.TableSource;
43
import com.yahoo.elide.modelconfig.store.models.ConfigFile;
44
import org.antlr.v4.runtime.tree.ParseTree;
45
import org.apache.commons.cli.CommandLine;
46
import org.apache.commons.cli.DefaultParser;
47
import org.apache.commons.cli.HelpFormatter;
48
import org.apache.commons.cli.Option;
49
import org.apache.commons.cli.Options;
50

51
import lombok.Getter;
52

53
import java.io.IOException;
54
import java.io.UncheckedIOException;
55
import java.util.ArrayList;
56
import java.util.Arrays;
57
import java.util.Collection;
58
import java.util.Collections;
59
import java.util.HashMap;
60
import java.util.HashSet;
61
import java.util.List;
62
import java.util.Locale;
63
import java.util.Map;
64
import java.util.Objects;
65
import java.util.Set;
66
import java.util.TreeSet;
67
import java.util.regex.Matcher;
68
import java.util.regex.Pattern;
69
import java.util.stream.Collectors;
70
import java.util.stream.Stream;
71

72
/**
73
 * Util class to validate and parse the config files. Optionally compiles config files.
74
 */
75
public class DynamicConfigValidator implements DynamicConfiguration, Validator {
76

77
    private static final Set<String> SQL_DISALLOWED_WORDS = new HashSet<>(
1✔
78
            Arrays.asList("DROP", "TRUNCATE", "DELETE", "INSERT", "UPDATE", "ALTER", "COMMENT", "CREATE", "DESCRIBE",
1✔
79
                    "SHOW", "USE", "GRANT", "REVOKE", "CONNECT", "LOCK", "EXPLAIN", "CALL", "MERGE", "RENAME"));
80
    private static final String SQL_SPLIT_REGEX = "\\s+";
81
    private static final String SEMI_COLON = ";";
82
    private static final Pattern HANDLEBAR_REGEX = Pattern.compile("<%(.*?)%>");
1✔
83

84
    @Getter private final ElideTableConfig elideTableConfig = new ElideTableConfig();
1✔
85
    @Getter private ElideSecurityConfig elideSecurityConfig;
86
    @Getter private Map<String, Object> modelVariables;
87
    private Map<String, Object> dbVariables;
88
    @Getter private final ElideDBConfig elideSQLDBConfig = new ElideSQLDBConfig();
1✔
89
    @Getter private final ElideNamespaceConfig elideNamespaceConfig = new ElideNamespaceConfig();
1✔
90
    private final DynamicConfigSchemaValidator schemaValidator = new DynamicConfigSchemaValidator();
1✔
91
    private final EntityDictionary dictionary;
92
    private final FileLoader fileLoader;
93

94
    private static final Pattern FILTER_VARIABLE_PATTERN = Pattern.compile(".*?\\{\\{(\\w+)\\}\\}");
1✔
95

96
    public DynamicConfigValidator(ClassScanner scanner, String configDir) {
97
        this(builder -> builder.scanner(scanner), configDir);
1✔
98
    }
1✔
99

100
    public DynamicConfigValidator(EntityDictionaryBuilderCustomizer entityDictionaryBuilderCustomizer,
101
            String configDir) {
102
        this(buildEntityDictionary(entityDictionaryBuilderCustomizer), configDir);
1✔
103
    }
1✔
104

105
    public DynamicConfigValidator(EntityDictionary dictionary, String configDir) {
1✔
106
        this.dictionary = dictionary;
1✔
107
        this.fileLoader = new FileLoader(configDir);
1✔
108
        initialize();
1✔
109
    }
1✔
110

111
    protected static EntityDictionary buildEntityDictionary(
112
            EntityDictionaryBuilderCustomizer entityDictionaryBuilderCustomizer) {
113
        EntityDictionary.EntityDictionaryBuilder  builder = EntityDictionary.builder();
1✔
114
        if (entityDictionaryBuilderCustomizer != null) {
1✔
115
            entityDictionaryBuilderCustomizer.customize(builder);
1✔
116
        }
117
        return builder.build();
1✔
118
    }
119

120
    private void initialize() {
121
        Set<Class<?>> annotatedClasses =
1✔
122
                        dictionary.getScanner().getAnnotatedClasses(Arrays.asList(Include.class, SecurityCheck.class));
1✔
123

124
        annotatedClasses.forEach(cls -> {
1✔
125
            if (cls.getAnnotation(Include.class) != null) {
1✔
126
                dictionary.bindEntity(cls);
1✔
127
            } else {
128
                dictionary.addSecurityCheck(cls);
×
129
            }
130
        });
1✔
131
    }
1✔
132

133
    public static void main(String[] args) {
134
        Options options = prepareOptions();
1✔
135

136
        try {
137
            CommandLine cli = new DefaultParser().parse(options, args);
1✔
138

139
            if (cli.hasOption("help")) {
1✔
140
                printHelp(options);
1✔
141
                System.exit(0);
×
142
            }
143
            if (!cli.hasOption("configDir")) {
1✔
144
                printHelp(options);
1✔
145
                System.err.println("Missing required option");
1✔
146
                System.exit(1);
×
147
            }
148
            String configDir = cli.getOptionValue("configDir");
1✔
149

150
            DynamicConfigValidator dynamicConfigValidator =
1✔
151
                    new DynamicConfigValidator(new DefaultClassScanner(), configDir);
152
            dynamicConfigValidator.readAndValidateConfigs();
1✔
153
            System.out.println("Configs Validation Passed!");
1✔
154
            System.exit(0);
×
155

156
        } catch (Exception e) {
1✔
157
            String msg = isBlank(e.getMessage()) ? "Process Failed!" : e.getMessage();
1✔
158
            System.err.println(msg);
1✔
159
            System.exit(2);
×
160
        }
×
161
    }
×
162

163
    @Override
164
    public void validate(Map<String, ConfigFile> resourceMap) {
165

166
        resourceMap.forEach((path, file) -> {
1✔
167
            if (file.getContent() == null || file.getContent().isEmpty()) {
1✔
168
                throw new BadRequestException(String.format("Null or empty file content for %s", file.getPath()));
1✔
169
            }
170

171
            //Validate that all the files are ones we know about and are safe to manipulate...
172
            if (file.getType().equals(ConfigFile.ConfigFileType.UNKNOWN)) {
1✔
173
                throw new BadRequestException(String.format("Unrecognized File: %s", file.getPath()));
×
174
            }
175

176
            if (path.contains("..")) {
1✔
177
                throw new BadRequestException(String.format("Parent directory traversal not allowed: %s",
×
178
                        file.getPath()));
×
179
            }
180

181
            //Validate that the file types and file paths match...
182
            if (! file.getType().equals(FileLoader.toType(path))) {
1✔
183
                throw new BadRequestException(String.format("File type %s does not match file path: %s",
×
184
                        file.getType(), file.getPath()));
×
185
            }
186
        });
1✔
187

188
        readConfigs(resourceMap);
1✔
189
        validateConfigs();
1✔
190
    }
1✔
191

192
    /**
193
     * Read and validate config files under config directory.
194
     * @throws IOException IOException
195
     */
196
    public void readAndValidateConfigs() throws IOException {
197
        Map<String, ConfigFile> loadedFiles = fileLoader.loadResources();
1✔
198

199
        validate(loadedFiles);
1✔
200
    }
1✔
201

202
    public void readConfigs() throws IOException {
203
        readConfigs(fileLoader.loadResources());
1✔
204
    }
1✔
205

206
    public void readConfigs(Map<String, ConfigFile> resourceMap) {
207
        this.modelVariables = readVariableConfig(Config.MODELVARIABLE, resourceMap);
1✔
208
        this.elideSecurityConfig = readSecurityConfig(resourceMap);
1✔
209
        this.dbVariables = readVariableConfig(Config.DBVARIABLE, resourceMap);
1✔
210
        this.elideSQLDBConfig.setDbconfigs(readDbConfig(resourceMap));
1✔
211
        this.elideTableConfig.setTables(readTableConfig(resourceMap));
1✔
212
        this.elideNamespaceConfig.setNamespaceconfigs(readNamespaceConfig(resourceMap));
1✔
213
        populateInheritance(this.elideTableConfig);
1✔
214
    }
1✔
215

216
    public void validateConfigs() {
217
        validateSecurityConfig();
1✔
218
        boolean configurationExists = validateRequiredConfigsProvided();
1✔
219

220
        if (configurationExists) {
1✔
221
            validateNameUniqueness(this.elideSQLDBConfig.getDbconfigs(),
1✔
222
                    "Multiple DB configs found with the same name: ");
223
            validateNameUniqueness(this.elideTableConfig.getTables(),
1✔
224
                    "Multiple Table configs found with the same name: ");
225
            validateTableConfig();
1✔
226
            validateNameUniqueness(this.elideNamespaceConfig.getNamespaceconfigs(),
1✔
227
                    "Multiple Namespace configs found with the same name: ");
228
            validateNamespaceConfig();
1✔
229
            validateJoinedTablesDBConnectionName(this.elideTableConfig);
1✔
230
        }
231
    }
1✔
232

233
    @Override
234
    public Set<Table> getTables() {
235
        return elideTableConfig.getTables();
×
236
    }
237

238
    @Override
239
    public Set<String> getRoles() {
240
        return elideSecurityConfig.getRoles();
×
241
    }
242

243
    @Override
244
    public Set<DBConfig> getDatabaseConfigurations() {
245
        return elideSQLDBConfig.getDbconfigs();
×
246
    }
247

248
    @Override
249
    public Set<NamespaceConfig> getNamespaceConfigurations() {
250
        return elideNamespaceConfig.getNamespaceconfigs();
×
251
    }
252

253
    private static void validateInheritance(ElideTableConfig tables) {
254
        tables.getTables().stream().forEach(table -> validateInheritance(tables, table, new HashSet<>()));
1✔
255
    }
1✔
256

257
    private static void validateInheritance(ElideTableConfig tables, Table table, Set<Table> visited) {
258
        visited.add(table);
1✔
259

260
        if (!table.hasParent()) {
1✔
261
            return;
1✔
262
        }
263
        Table parent = table.getParent(tables);
1✔
264
        if (parent == null) {
1✔
265
            throw new IllegalStateException(
1✔
266
                    "Undefined model: " + table.getExtend() + " is used as a Parent(extend) for another model.");
1✔
267
        }
268
        if (visited.contains(parent)) {
1✔
269
            throw new IllegalStateException(
1✔
270
                    String.format("Inheriting from table '%s' creates an illegal cyclic dependency.",
1✔
271
                            parent.getName()));
1✔
272
        }
273
        validateInheritance(tables, parent, visited);
1✔
274
    }
1✔
275

276
    private void populateInheritance(ElideTableConfig elideTableConfig) {
277
        //ensures validation is run before populate always.
278
        validateInheritance(this.elideTableConfig);
1✔
279

280
        Set<Table> processed = new HashSet<>();
1✔
281
        elideTableConfig.getTables().stream().forEach(table -> populateInheritance(table, processed));
1✔
282
    }
1✔
283

284
    private void populateInheritance(Table table, Set<Table> processed) {
285
        if (processed.contains(table)) {
1✔
UNCOV
286
            return;
×
287
        }
288

289
        processed.add(table);
1✔
290

291
        if (!table.hasParent()) {
1✔
292
            return;
1✔
293
        }
294

295
        Table parent = table.getParent(this.elideTableConfig);
1✔
296
        if (!processed.contains(parent)) {
1✔
UNCOV
297
            populateInheritance(parent, processed);
×
298
        }
299

300
        Map<String, Measure> measures = getInheritedMeasures(parent, attributesListToMap(table.getMeasures()));
1✔
301
        table.setMeasures(new ArrayList<>(measures.values()));
1✔
302

303
        Map<String, Dimension> dimensions = getInheritedDimensions(parent, attributesListToMap(table.getDimensions()));
1✔
304
        table.setDimensions(new ArrayList<>(dimensions.values()));
1✔
305

306
        Map<String, Join> joins = getInheritedJoins(parent, attributesListToMap(table.getJoins()));
1✔
307
        table.setJoins(new ArrayList<>(joins.values()));
1✔
308

309
        String schema = getInheritedSchema(parent, table.getSchema());
1✔
310
        table.setSchema(schema);
1✔
311

312
        String dbConnectionName = getInheritedConnection(parent, table.getDbConnectionName());
1✔
313
        table.setDbConnectionName(dbConnectionName);
1✔
314

315
        String sql = getInheritedSql(parent, table.getSql());
1✔
316
        table.setSql(sql);
1✔
317

318
        String tableName = getInheritedTable(parent, table.getTable());
1✔
319
        table.setTable(tableName);
1✔
320

321
        List<Argument> arguments = getInheritedArguments(parent, table.getArguments());
1✔
322
        table.setArguments(arguments);
1✔
323
        // isFact, isHidden, ReadAccess, namespace have default Values in schema, so can not be inherited.
324
        // Other properties (tags, cardinality, etc.) have been categorized as non-inheritable too.
325
    }
1✔
326

327
    private <T extends Named> Map<String, T> attributesListToMap(List<T> attributes) {
328
        return attributes.stream().collect(Collectors.toMap(T::getName, attribute -> attribute));
1✔
329
    }
330

331
    @FunctionalInterface
332
    public interface Inheritance<T> {
333
        public T inherit();
334
    }
335

336
    private Map<String, Measure> getInheritedMeasures(Table table, Map<String, Measure> measures) {
337
        Inheritance<?> action = () -> {
1✔
338
            table.getMeasures().forEach(measure -> {
1✔
339
                if (!measures.containsKey(measure.getName())) {
1✔
340
                    measures.put(measure.getName(), measure);
1✔
341
                }
342
            });
1✔
343
            return measures;
1✔
344
        };
345

346
        action.inherit();
1✔
347
        return measures;
1✔
348
    }
349

350
    private Map<String, Dimension> getInheritedDimensions(Table table, Map<String, Dimension> dimensions) {
351
        Inheritance<?> action = () -> {
1✔
352
            table.getDimensions().forEach(dimension -> {
1✔
353
                if (!dimensions.containsKey(dimension.getName())) {
1✔
354
                    dimensions.put(dimension.getName(), dimension);
1✔
355
                }
356
            });
1✔
357
            return dimensions;
1✔
358
        };
359

360
        action.inherit();
1✔
361
        return dimensions;
1✔
362
    }
363

364
    private Map<String, Join> getInheritedJoins(Table table, Map<String, Join> joins) {
365
        Inheritance<?> action = () -> {
1✔
366
            table.getJoins().forEach(join -> {
1✔
367
                if (!joins.containsKey(join.getName())) {
1✔
368
                    joins.put(join.getName(), join);
1✔
369
                }
370
            });
1✔
371
            return joins;
1✔
372
        };
373

374
        action.inherit();
1✔
375
        return joins;
1✔
376
    }
377

378
    private <T> T getInheritedAttribute(Inheritance<T> action, T property) {
379
        return property == null ? action.inherit() : property;
1✔
380
    }
381

382
    private <T extends Collection<?>> T getInheritedAttribute(Inheritance<T> action, T property) {
383
        return property == null || property.isEmpty() ? action.inherit() : property;
1✔
384
    }
385

386
    private String getInheritedSchema(Table table, String schema) {
387
        return getInheritedAttribute(table::getSchema, schema);
1✔
388
    }
389

390
    private String getInheritedConnection(Table table, String connection) {
391
        return getInheritedAttribute(table::getDbConnectionName, connection);
1✔
392
    }
393

394
    private String getInheritedSql(Table table, String sql) {
395
        return getInheritedAttribute(table::getSql, sql);
1✔
396
    }
397

398
    private String getInheritedTable(Table table, String tableName) {
399
        return getInheritedAttribute(table::getTable, tableName);
1✔
400
    }
401

402
    private List<Argument> getInheritedArguments(Table table, List<Argument> arguments) {
403
        return getInheritedAttribute(table::getArguments, arguments);
1✔
404
    }
405

406
    /**
407
     * Read variable file config.
408
     * @param config Config Enum
409
     * @return Map<String, Object> A map containing all the variables if variable config exists else empty map
410
     */
411
    private Map<String, Object> readVariableConfig(Config config, Map<String, ConfigFile> resourceMap) {
412

413
        return resourceMap
1✔
414
                        .entrySet()
1✔
415
                        .stream()
1✔
416
                        .filter(entry -> entry.getKey().startsWith(config.getConfigPath()))
1✔
417
                        .map(entry -> {
1✔
418
                            try {
419
                                return DynamicConfigHelpers.stringToVariablesPojo(entry.getKey(),
1✔
420
                                                entry.getValue().getContent(), schemaValidator);
1✔
421
                            } catch (IOException e) {
×
422
                                throw new UncheckedIOException(e.getMessage(), e);
×
423
                            }
424
                        })
425
                        .findFirst()
1✔
426
                        .orElse(new HashMap<>());
1✔
427
    }
428

429
    /**
430
     * Read and validates security config file.
431
     */
432
    private ElideSecurityConfig readSecurityConfig(Map<String, ConfigFile> resourceMap) {
433

434
        return resourceMap
1✔
435
                        .entrySet()
1✔
436
                        .stream()
1✔
437
                        .filter(entry -> entry.getKey().startsWith(Config.SECURITY.getConfigPath()))
1✔
438
                        .map(entry -> {
1✔
439
                            try {
440
                                String content = entry.getValue().getContent();
1✔
441
                                validateConfigForMissingVariables(content, this.modelVariables);
1✔
442
                                return DynamicConfigHelpers.stringToElideSecurityPojo(entry.getKey(),
1✔
443
                                                content, this.modelVariables, schemaValidator);
444
                            } catch (IOException e) {
1✔
445
                                throw new UncheckedIOException(e.getMessage(), e);
1✔
446
                            }
447
                        })
448
                        .findAny()
1✔
449
                        .orElse(new ElideSecurityConfig());
1✔
450
    }
451

452
    /**
453
     * Read and validates db config files.
454
     * @return Set<DBConfig> Set of SQL DB Configs
455
     */
456
    private Set<DBConfig> readDbConfig(Map<String, ConfigFile> resourceMap) {
457

458
        return resourceMap
1✔
459
                        .entrySet()
1✔
460
                        .stream()
1✔
461
                        .filter(entry -> entry.getKey().startsWith(Config.SQLDBConfig.getConfigPath()))
1✔
462
                        .map(entry -> {
1✔
463
                            try {
464
                                String content = entry.getValue().getContent();
1✔
465
                                validateConfigForMissingVariables(content, this.dbVariables);
1✔
466
                                return DynamicConfigHelpers.stringToElideDBConfigPojo(entry.getKey(),
1✔
467
                                                content, this.dbVariables, schemaValidator);
468
                            } catch (IOException e) {
×
469
                                throw new UncheckedIOException(e.getMessage(), e);
×
470
                            }
471
                        })
472
                        .flatMap(dbconfig -> dbconfig.getDbconfigs().stream())
1✔
473
                        .collect(Collectors.toSet());
1✔
474
    }
475

476
    /**
477
     * Read and validates namespace config files.
478
     * @return Set<NamespaceConfig> Set of Namespace Configs
479
     */
480
    private Set<NamespaceConfig> readNamespaceConfig(Map<String, ConfigFile> resourceMap) {
481

482
        return resourceMap
1✔
483
                        .entrySet()
1✔
484
                        .stream()
1✔
485
                        .filter(entry -> entry.getKey().startsWith(Config.NAMESPACEConfig.getConfigPath()))
1✔
486
                        .map(entry -> {
1✔
487
                            try {
488
                                String content = entry.getValue().getContent();
1✔
489
                                validateConfigForMissingVariables(content, this.modelVariables);
1✔
490
                                String fileName = entry.getKey();
1✔
491
                                return DynamicConfigHelpers.stringToElideNamespaceConfigPojo(fileName,
1✔
492
                                                content, this.modelVariables, schemaValidator);
493
                            } catch (IOException e) {
1✔
494
                                throw new UncheckedIOException(e.getMessage(), e);
1✔
495
                            }
496
                        })
497
                        .flatMap(namespaceconfig -> namespaceconfig.getNamespaceconfigs().stream())
1✔
498
                        .collect(Collectors.toSet());
1✔
499
    }
500

501
    /**
502
     * Read and validates table config files.
503
     */
504
    private Set<Table> readTableConfig(Map<String, ConfigFile> resourceMap) {
505

506
        return resourceMap
1✔
507
                        .entrySet()
1✔
508
                        .stream()
1✔
509
                        .filter(entry -> entry.getKey().startsWith(Config.TABLE.getConfigPath()))
1✔
510
                        .map(entry -> {
1✔
511
                            try {
512
                                String content = entry.getValue().getContent();
1✔
513
                                validateConfigForMissingVariables(content, this.modelVariables);
1✔
514
                                return DynamicConfigHelpers.stringToElideTablePojo(entry.getKey(),
1✔
515
                                                content, this.modelVariables, schemaValidator);
516
                            } catch (IOException e) {
1✔
517
                                throw new UncheckedIOException(e.getMessage(), e);
1✔
518
                            }
519
                        })
520
                        .flatMap(table -> table.getTables().stream())
1✔
521
                        .collect(Collectors.toSet());
1✔
522
    }
523

524
    /**
525
     * Checks if neither Table nor DB config files provided.
526
     */
527
    private boolean validateRequiredConfigsProvided() {
528
        return !(this.elideTableConfig.getTables().isEmpty() && this.elideSQLDBConfig.getDbconfigs().isEmpty());
1✔
529
    }
530

531
    /**
532
     * Extracts any handlebar variables in config file and checks if they are
533
     * defined in variable config. Throw exception for undefined variables.
534
     * @param config config file
535
     * @param variables A map of defined variables
536
     */
537
    private static void validateConfigForMissingVariables(String config, Map<String, Object> variables) {
538
        Matcher regexMatcher = HANDLEBAR_REGEX.matcher(config);
1✔
539
        while (regexMatcher.find()) {
1✔
540
            String str = regexMatcher.group(1).trim();
1✔
541
            if (!variables.containsKey(str)) {
1✔
542
                throw new IllegalStateException(str + " is used as a variable in either table or security config files "
1✔
543
                        + "but is not defined in variables config file.");
544
            }
545
        }
1✔
546
    }
1✔
547

548
    /**
549
     * Validate table configs.
550
     * @return boolean true if all provided table properties passes validation
551
     */
552
    private boolean validateTableConfig() {
553
        Set<String> extractedFieldChecks = new HashSet<>();
1✔
554
        Set<String> extractedTableChecks = new HashSet<>();
1✔
555
        PermissionExpressionVisitor visitor = new PermissionExpressionVisitor();
1✔
556

557
        for (Table table : elideTableConfig.getTables()) {
1✔
558

559
            validateSql(table.getSql());
1✔
560
            validateArguments(table, table.getArguments(), table.getFilterTemplate());
1✔
561
            //TODO - once tables support versions - replace NO_VERSION with apiVersion
562
            validateNamespaceExists(table.getNamespace(), NO_VERSION);
1✔
563
            Set<String> tableFields = new HashSet<>();
1✔
564

565
            table.getDimensions().forEach(dim -> {
1✔
566
                validateFieldNameUniqueness(tableFields, dim.getName(), table.getName());
1✔
567
                validateSql(dim.getDefinition());
1✔
568
                validateTableSource(dim.getTableSource());
1✔
569
                validateArguments(table, dim.getArguments(), dim.getFilterTemplate());
1✔
570
                extractChecksFromExpr(dim.getReadAccess(), extractedFieldChecks, visitor);
1✔
571
            });
1✔
572

573
            table.getMeasures().forEach(measure -> {
1✔
574
                validateFieldNameUniqueness(tableFields, measure.getName(), table.getName());
1✔
575
                validateSql(measure.getDefinition());
1✔
576
                validateArguments(table, measure.getArguments(), measure.getFilterTemplate());
1✔
577
                extractChecksFromExpr(measure.getReadAccess(), extractedFieldChecks, visitor);
1✔
578
            });
1✔
579

580
            table.getJoins().forEach(join -> {
1✔
581
                validateFieldNameUniqueness(tableFields, join.getName(), table.getName());
1✔
582
                validateSql(join.getDefinition());
1✔
583
                validateModelExists(join.getTo());
1✔
584
                //TODO - once tables support versions - replace NO_VERSION with apiVersion
585
                validateNamespaceExists(join.getNamespace(), NO_VERSION);
1✔
586
            });
1✔
587

588
            extractChecksFromExpr(table.getReadAccess(), extractedTableChecks, visitor);
1✔
589
        }
1✔
590

591
        validateChecks(extractedTableChecks, extractedFieldChecks);
1✔
592

593
        return true;
1✔
594
    }
595

596
    /**
597
     * Validate namespace configs.
598
     * @return boolean true if all provided namespace properties passes validation
599
     */
600
    private boolean validateNamespaceConfig() {
601
        Set<String> extractedChecks = new HashSet<>();
1✔
602
        PermissionExpressionVisitor visitor = new PermissionExpressionVisitor();
1✔
603

604
        for (NamespaceConfig namespace : elideNamespaceConfig.getNamespaceconfigs()) {
1✔
605
            extractChecksFromExpr(namespace.getReadAccess(), extractedChecks, visitor);
1✔
606
        }
1✔
607

608
        validateChecks(extractedChecks, Collections.emptySet());
1✔
609

610
        return true;
1✔
611
    }
612

613
    private void validateArguments(Table table, List<Argument> arguments, String requiredFilter) {
614
        List<Argument> allArguments = new ArrayList<>(arguments);
1✔
615

616
        /* Check for table arguments added in the required filter template */
617
        if (requiredFilter != null) {
1✔
618
            Matcher matcher = FILTER_VARIABLE_PATTERN.matcher(requiredFilter);
1✔
619
            while (matcher.find()) {
1✔
620
                allArguments.add(Argument.builder()
1✔
621
                        .name(matcher.group(1))
1✔
622
                        .build());
1✔
623
            }
624
        }
625

626
        validateNameUniqueness(allArguments, "Multiple Arguments found with the same name: ");
1✔
627
        arguments.forEach(arg -> validateTableSource(arg.getTableSource()));
1✔
628
    }
1✔
629

630
    private void validateChecks(Set<String> tableChecks, Set<String> fieldChecks) {
631

632
        if (tableChecks.isEmpty() && fieldChecks.isEmpty()) {
1✔
633
            return; // Nothing to validate
1✔
634
        }
635

636
        Set<String> staticChecks = dictionary.getCheckIdentifiers();
1✔
637

638
        List<String> undefinedChecks = Stream.concat(tableChecks.stream(), fieldChecks.stream())
1✔
639
                        .filter(check -> !(elideSecurityConfig.hasCheckDefined(check) || staticChecks.contains(check)))
1✔
640
                        .sorted()
1✔
641
                        .collect(Collectors.toList());
1✔
642

643
        if (!undefinedChecks.isEmpty()) {
1✔
644
            throw new IllegalStateException("Found undefined security checks: " + undefinedChecks);
1✔
645
        }
646

647
        tableChecks.stream()
1✔
648
                .filter(check -> dictionary.getCheckMappings().containsKey(check))
1✔
649
                .forEach(check -> {
1✔
650
                    Class<? extends Check> checkClass = dictionary.getCheck(check);
×
651
                    //Validates if the permission check either user Check or FilterExpressionCheck Check
652
                    if (!(UserCheck.class.isAssignableFrom(checkClass)
×
653
                            || FilterExpressionCheck.class.isAssignableFrom(checkClass))) {
×
654
                        throw new IllegalStateException("Table or Namespace cannot have Operation Checks. Given: "
×
655
                                + checkClass);
656
                    }
657
                });
×
658
        fieldChecks.stream()
1✔
659
                .filter(check -> dictionary.getCheckMappings().containsKey(check))
1✔
660
                .forEach(check -> {
1✔
661
                    Class<? extends Check> checkClass = dictionary.getCheck(check);
×
662
                    //Validates if the permission check is User check
663
                    if (!UserCheck.class.isAssignableFrom(checkClass)) {
×
664
                        throw new IllegalStateException("Field can only have User checks or Roles. Given: "
×
665
                                + checkClass);
666
                    }
667
                });
×
668
    }
1✔
669

670
    private static void extractChecksFromExpr(String readAccess, Set<String> extractedChecks,
671
                    PermissionExpressionVisitor visitor) {
672
        if (isNotBlank(readAccess)) {
1✔
673
            ParseTree root = EntityPermissions.parseExpression(readAccess);
1✔
674
            extractedChecks.addAll(visitor.visit(root));
1✔
675
        }
676
    }
1✔
677

678
    private static void validateFieldNameUniqueness(Set<String> alreadyFoundFields, String fieldName,
679
                    String tableName) {
680
        if (!alreadyFoundFields.add(fieldName.toLowerCase(Locale.ENGLISH))) {
1✔
681
            throw new IllegalStateException(String.format("Duplicate!! Field name: %s is not unique for table: %s",
×
682
                            fieldName, tableName));
683
        }
684
    }
1✔
685

686
    /**
687
     * Validates tableSource is in format: modelName.logicalColumnName and refers to a defined model and a defined
688
     * column with in that model.
689
     */
690
    private void validateTableSource(TableSource tableSource) {
691
        if (tableSource == null) {
1✔
692
            return; // Nothing to validate
1✔
693
        }
694

695
        String modelName = Table.getModelName(tableSource.getTable(), tableSource.getNamespace());
1✔
696

697
        if (elideTableConfig.hasTable(modelName)) {
1✔
698
            Table lookupTable = elideTableConfig.getTable(modelName);
1✔
699
            if (!lookupTable.hasField(tableSource.getColumn())) {
1✔
700
                throw new IllegalStateException("Invalid tableSource : "
×
701
                        + tableSource
702
                        + " . Field : "
703
                        + tableSource.getColumn()
×
704
                        + " is undefined for hjson model: "
705
                        + tableSource.getTable());
×
706
            }
707
            return;
1✔
708
        }
709

710
        //TODO - once tables support versions - replace NO_VERSION with apiVersion
711
        if (hasStaticModel(modelName, NO_VERSION)) {
×
712
            if (!hasStaticField(modelName, NO_VERSION, tableSource.getColumn())) {
×
713
                throw new IllegalStateException("Invalid tableSource : " + tableSource
×
714
                        + " . Field : " + tableSource.getColumn()
×
715
                        + " is undefined for non-hjson model: " + tableSource.getTable());
×
716
            }
717
            return;
×
718
        }
719

720
        throw new IllegalStateException("Invalid tableSource : " + tableSource
×
721
                + " . Undefined model: " + tableSource.getTable());
×
722
    }
723

724
    /**
725
     * Validates join clause does not refer to a Table which is not in the same DBConnection. If joined table is not
726
     * part of dynamic configuration, then ignore
727
     */
728
    private static void validateJoinedTablesDBConnectionName(ElideTableConfig elideTableConfig) {
729

730
        for (Table table : elideTableConfig.getTables()) {
1✔
731
            if (!table.getJoins().isEmpty()) {
1✔
732

733
                Set<String> joinedTables = table.getJoins()
1✔
734
                        .stream()
1✔
735
                        //TODO - NOT SURE
736
                        .map(Join::getTo)
1✔
737
                        .collect(Collectors.toSet());
1✔
738

739
                Set<String> connections = elideTableConfig.getTables()
1✔
740
                        .stream()
1✔
741
                        .filter(t -> joinedTables.contains(t.getGlobalName()))
1✔
742
                        .map(Table::getDbConnectionName)
1✔
743
                        .collect(Collectors.toSet());
1✔
744

745
                if (connections.size() > 1 || (connections.size() == 1
1✔
746
                                && !Objects.equals(table.getDbConnectionName(), connections.iterator().next()))) {
1✔
747
                    throw new IllegalStateException("DBConnection name mismatch between table: " + table.getName()
1✔
748
                                    + " and tables in its Join Clause.");
749
                }
750
            }
751
        }
1✔
752
    }
1✔
753

754
    /**
755
     * Validates table (or db connection) name is unique across all the dynamic table (or db connection) configs.
756
     */
757
    public static void validateNameUniqueness(Collection<? extends Named> configs, String errorMsg) {
758

759
        Set<String> names = new HashSet<>();
1✔
760
        configs.forEach(obj -> {
1✔
761
            if (!names.add(obj.getGlobalName().toLowerCase(Locale.ENGLISH))) {
1✔
762
                throw new IllegalStateException(errorMsg + obj.getGlobalName());
1✔
763
            }
764
        });
1✔
765
    }
1✔
766

767
    /**
768
     * Check if input sql definition contains either semicolon or any of disallowed
769
     * keywords. Throw exception if check fails.
770
     */
771
    private static void validateSql(String sqlDefinition) {
772
        if (isNotBlank(sqlDefinition) && (sqlDefinition.contains(SEMI_COLON)
1✔
773
                || containsDisallowedWords(sqlDefinition, SQL_SPLIT_REGEX, SQL_DISALLOWED_WORDS))) {
1✔
774
            throw new IllegalStateException("sql/definition provided in table config contain either '" + SEMI_COLON
1✔
775
                    + "' or one of these words: " + Arrays.toString(SQL_DISALLOWED_WORDS.toArray()));
1✔
776
        }
777
    }
1✔
778

779
    /**
780
     * Validate role name provided in security config.
781
     * @return boolean true if all role name passes validation else throw exception
782
     */
783
    private boolean validateSecurityConfig() {
784
        Set<String> alreadyDefinedRoles = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
1✔
785
        alreadyDefinedRoles.addAll(dictionary.getCheckIdentifiers());
1✔
786

787
        elideSecurityConfig.getRoles().forEach(role -> {
1✔
788
            if (alreadyDefinedRoles.contains(role)) {
1✔
789
                throw new IllegalStateException(String.format(
1✔
790
                                "Duplicate!! Role name: '%s' is already defined. Please use different role.", role));
791
            }
792
            alreadyDefinedRoles.add(role);
1✔
793
        });
1✔
794

795
        return true;
1✔
796
    }
797

798
    private void validateModelExists(String name) {
799
        if (!(elideTableConfig.hasTable(name) || hasStaticModel(name, NO_VERSION))) {
1✔
800
            throw new IllegalStateException(
1✔
801
                            "Model: " + name + " is neither included in dynamic models nor in static models");
802
        }
803
    }
1✔
804

805
    private void validateNamespaceExists(String name, String version) {
806
        if (!elideNamespaceConfig.hasNamespace(name, version)) {
1✔
807
            throw new IllegalStateException(
1✔
808
                            "Namespace: " + name + " is not included in dynamic configs");
809
        }
810
    }
1✔
811

812
    /**
813
     * Checks if any word in the input string matches any of the disallowed words.
814
     * @param str input string to validate
815
     * @param splitter regex for splitting input string
816
     * @param keywords Set of disallowed words
817
     * @return boolean true if any word in the input string matches any of the
818
     *         disallowed words else false
819
     */
820
    private static boolean containsDisallowedWords(String str, String splitter, Set<String> keywords) {
821
        return isNotBlank(str)
1✔
822
                && Arrays.stream(str.trim().toUpperCase(Locale.ENGLISH).split(splitter)).anyMatch(keywords::contains);
1✔
823
    }
824

825
    /**
826
     * Define Arguments.
827
     */
828
    private static final Options prepareOptions() {
829
        Options options = new Options();
1✔
830
        options.addOption(new Option("h", "help", false, "Print a help message and exit."));
1✔
831
        options.addOption(new Option("c", "configDir", true,
1✔
832
                "Path for Configs Directory.\n"
833
                        + "Expected Directory Structure under Configs Directory:\n"
834
                        + "./models/security.hjson(optional)\n"
835
                        + "./models/variables.hjson(optional)\n"
836
                        + "./models/tables/(optional)\n"
837
                        + "./models/tables/table1.hjson\n"
838
                        + "./models/tables/table2.hjson\n"
839
                        + "./models/tables/tableN.hjson\n"
840
                        + "./db/variables.hjson(optional)\n"
841
                        + "./db/sql/(optional)\n"
842
                        + "./db/sql/db1.hjson\n"
843
                        + "./db/sql/db2.hjson\n"
844
                        + "./db/sql/dbN.hjson\n"));
845

846
        return options;
1✔
847
    }
848

849
    /**
850
     * Print Help.
851
     */
852
    private static void printHelp(Options options) {
853
        HelpFormatter formatter = new HelpFormatter();
1✔
854
        formatter.printHelp(
1✔
855
                "java -cp <Jar File> com.yahoo.elide.modelconfig.validator.DynamicConfigValidator",
856
                options);
857
    }
1✔
858

859
    private boolean hasStaticField(String modelName, String version, String fieldName) {
860
        Type<?> modelType = dictionary.getEntityClass(modelName, version);
×
861
        if (modelType == null) {
×
862
            return false;
×
863
        }
864

865
        try {
866
            return (modelType.getDeclaredField(fieldName) != null);
×
867
        } catch (NoSuchFieldException e) {
×
868
            return false;
×
869
        }
870
    }
871

872
    private boolean hasStaticModel(String modelName, String version) {
873
        Type<?> modelType = dictionary.getEntityClass(modelName, version);
1✔
874
        return modelType != null;
1✔
875
    }
876
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc