001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyrighted [year] [name of copyright owner]".
013 *
014 * Copyright 2011-2016 ForgeRock AS.
015 */
016
017package org.forgerock.json.schema;
018
019import static org.forgerock.json.JsonValueFunctions.uri;
020import static org.kohsuke.args4j.ExampleMode.ALL;
021import static org.kohsuke.args4j.ExampleMode.REQUIRED;
022
023import java.io.Console;
024import java.io.File;
025import java.io.FileFilter;
026import java.io.FileInputStream;
027import java.io.FileNotFoundException;
028import java.io.IOException;
029import java.net.URI;
030import java.net.URISyntaxException;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Scanner;
037
038import com.fasterxml.jackson.databind.ObjectMapper;
039import org.forgerock.json.JsonValue;
040import org.forgerock.json.schema.validator.Constants;
041import org.forgerock.json.schema.validator.ErrorHandler;
042import org.forgerock.json.schema.validator.FailFastErrorHandler;
043import org.forgerock.json.schema.validator.ObjectValidatorFactory;
044import org.forgerock.json.schema.validator.exceptions.SchemaException;
045import org.forgerock.json.schema.validator.exceptions.ValidationException;
046import org.forgerock.json.schema.validator.validators.Validator;
047import org.kohsuke.args4j.Argument;
048import org.kohsuke.args4j.CmdLineException;
049import org.kohsuke.args4j.CmdLineParser;
050import org.kohsuke.args4j.Option;
051
052/**
053 * Command-line interface to manipulate schemas.
054 */
055public final class Main {
056
057
058    private static final ObjectMapper MAPPER = new ObjectMapper();
059    private static final String ROOT_SCHEMA_ID = "http://www.forgerock.org/schema/";
060
061    private final Map<URI, Validator> schemaCache = new HashMap<>();
062
063    @Option(name = "-v", aliases = {"--verbose"}, usage = "display all validation error not just the first")
064    private boolean verbose;
065
066    @Option(name = "-s", aliases = {"--schemas"}, required = true, usage = "file or folder contains the schema(s)",
067            metaVar = "./schema")
068    private File schemaFile = new File("./schema");
069
070    @Option(name = "-b", aliases = {"--base"}, metaVar = ROOT_SCHEMA_ID,
071            usage = "base value to resolve relative schema IDs. Default: " + ROOT_SCHEMA_ID)
072    private String schemeBase = ROOT_SCHEMA_ID;
073
074    @Option(name = "-i", aliases = {"--id"},
075            usage = "id of the schema. Optional if the object has \"$schema\" property")
076    private String schemaURI;
077
078    @Option(name = "-f", aliases = {"--file"}, usage = "input from this file", metaVar = "sample.json")
079    private File inputFile;
080
081    // receives other command line parameters than options
082    @Argument
083    private List<String> arguments = new ArrayList<>();
084
085    /**
086     * Entry point.
087     * @param args The CLI args.
088     * @throws Exception On failure.
089     */
090    public static void main(String[] args) throws Exception {
091        new Main().doMain(args);
092    }
093
094    private void doMain(String[] args) throws Exception {
095        CmdLineParser parser = new CmdLineParser(this);
096
097        // if you have a wider console, you could increase the value;
098        // here 80 is also the default
099        parser.setUsageWidth(80);
100
101        try {
102            // parse the arguments.
103            parser.parseArgument(args);
104
105            // you can parse additional arguments if you want.
106            // parser.parseArgument("more","args");
107
108            // after parsing arguments, you should check
109            // if enough arguments are given.
110            /*if (arguments.isEmpty())
111                throw new CmdLineException(parser, "No argument is given");*/
112
113        } catch (CmdLineException e) {
114            // if there's a problem in the command line,
115            // you'll get this exception. this will report
116            // an error message.
117            System.err.println(e.getMessage());
118            System.err.println("java Main [options...] arguments...");
119            // print the list of available options
120            parser.printUsage(System.err);
121            System.err.println();
122
123            // print option sample. This is useful some time
124            System.err.println("  Example: java Main" + parser.printExample(REQUIRED));
125            System.err.println("  Example: java Main" + parser.printExample(ALL));
126
127            return;
128        }
129
130        // set the base for all relative schema
131        URI base = new URI(schemeBase);
132        if (!base.isAbsolute()) {
133            throw new IllegalArgumentException("-b (-base) must be an absolute URI");
134        }
135
136        // load all schema
137        init(base);
138
139        if (null == inputFile) {
140            while (true) {
141                try {
142                    validate(loadFromConsole());
143                } catch (Exception e) {
144                    printOutException(e);
145                }
146            }
147        } else {
148            try {
149                validate(loadFromFile());
150            } catch (Exception e) {
151                printOutException(e);
152            }
153        }
154    }
155
156    //Initialization
157
158    private void init(URI base) throws IOException {
159        System.out.append("Loading schemas from: ")
160                .append(schemaFile.getAbsolutePath())
161                .append(" with base ")
162                .append(base.toString())
163                .println(" URI");
164        if (schemaFile.isDirectory()) {
165            validateDirectory(schemaFile);
166            FileFilter filter = new FileFilter() {
167
168                public boolean accept(File f) {
169                    return (f.isDirectory()) || (f.getName().endsWith(".json"));
170                }
171            };
172
173            for (File f : getFileListingNoSort(schemaFile, filter)) {
174                //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6226081
175                //org.apache.http.client.utils.URIUtils.resolve(URI,URI)
176                URI relative = schemaFile.toURI().relativize(f.toURI());
177                loadSchema(base.resolve(relative), f);
178            }
179        } else if (schemaFile.isFile()) {
180            loadSchema(base, schemaFile);
181        } else {
182            System.exit(1);
183        }
184    }
185
186    private void loadSchema(URI base, File schemaFile) throws IOException {
187        JsonValue schemaMap = new JsonValue(MAPPER.readValue(new FileInputStream(schemaFile), Map.class));
188        URI id = schemaMap.get(Constants.ID).required().as(uri());
189        Validator v = ObjectValidatorFactory.getTypeValidator(schemaMap.asMap());
190        if (!id.isAbsolute()) {
191            id = base.resolve(id);
192        }
193        schemaCache.put(id, v);
194        System.out.append("Schema ").append(id.toString()).println(" loaded from file:");
195        System.out.append("     location: ").println(schemaFile.getAbsolutePath());
196    }
197
198    /**
199     * Recursively walk a directory tree and return a List of all
200     * Files found; the List is sorted using File.compareTo().
201     *
202     * @param aStartingDir is a valid directory, which can be read.
203     * @param filter
204     * @return
205     * @throws java.io.FileNotFoundException
206     */
207    private List<File> getFileListingNoSort(File aStartingDir, FileFilter filter) throws FileNotFoundException {
208        List<File> result = new ArrayList<>();
209        List<File> filesDirs = Arrays.asList(aStartingDir.listFiles(filter));
210        for (File file : filesDirs) {
211            if (!file.isFile()) {
212                //must be a directory
213                //recursive call!
214                List<File> deeperList = getFileListingNoSort(file, filter);
215                result.addAll(deeperList);
216            } else {
217                result.add(file);
218            }
219        }
220        return result;
221    }
222
223    /**
224     * Directory is valid if it exists, does not represent a file, and can be read.
225     *
226     * @param aDirectory
227     * @throws java.io.FileNotFoundException
228     */
229    private void validateDirectory(File aDirectory) throws FileNotFoundException {
230        if (aDirectory == null) {
231            throw new IllegalArgumentException("Directory should not be null.");
232        }
233        if (!aDirectory.exists()) {
234            throw new FileNotFoundException("Directory does not exist: " + aDirectory);
235        }
236        if (!aDirectory.isDirectory()) {
237            throw new IllegalArgumentException("Is not a directory: " + aDirectory);
238        }
239        if (!aDirectory.canRead()) {
240            throw new IllegalArgumentException("Directory cannot be read: " + aDirectory);
241        }
242    }
243
244    //Validation
245
246    private void validate(JsonValue value) throws SchemaException, URISyntaxException {
247        URI schemaId = value.get(Constants.SCHEMA).as(uri());
248        if (null == schemaId && isEmptyOrBlank(schemaURI)) {
249            System.out.println("-i (--id) must be an URI");
250            return;
251        } else if (null == schemaId) {
252            schemaId = new URI(schemaURI);
253        }
254
255        Validator validator = schemaCache.get(schemaId);
256        if (null != validator) {
257            if (verbose) {
258                final boolean[] valid = new boolean[1];
259                validator.validate(value.getObject(), null, new ErrorHandler() {
260                    @Override
261                    public void error(ValidationException exception) throws SchemaException {
262                        valid[0] = false;
263                        printOutException(exception);
264                    }
265
266                    @Override
267                    @Deprecated
268                    public void assembleException() throws ValidationException {
269                    }
270                });
271                if (valid.length == 0) {
272                    System.out.println("OK - Object is valid!");
273                }
274            } else {
275                validator.validate(value.getObject(), null, new FailFastErrorHandler());
276                System.out.println("OK - Object is valid!");
277            }
278        } else {
279            System.out.append("Schema ").append(schemaId.toString()).println(" not found!");
280        }
281    }
282
283    private JsonValue loadFromConsole() throws IOException {
284        System.out.println();
285        System.out.println("> Enter 'exit' and press enter to exit");
286        System.out.println("> Press ctrl-D to finish input");
287        System.out.println("Start data input:");
288        String input = null;
289        StringBuilder stringBuilder = new StringBuilder();
290        Console c = System.console();
291        if (c == null) {
292            System.err.println("No console.");
293            System.exit(1);
294        }
295
296        Scanner scanner = new Scanner(c.reader());
297
298        while (scanner.hasNext()) {
299            input = scanner.next();
300            if (null == input) {
301                //control-D pressed
302                break;
303            } else if ("exit".equalsIgnoreCase(input)) {
304                System.exit(0);
305            } else {
306                stringBuilder.append(input);
307            }
308        }
309        return new JsonValue(MAPPER.readValue(stringBuilder.toString(), Object.class));
310    }
311
312    private JsonValue loadFromFile() throws IOException {
313        return new JsonValue(MAPPER.readValue(inputFile, Object.class));
314    }
315
316
317    private static boolean isEmptyOrBlank(String str) {
318        return str == null || str.trim().isEmpty();
319    }
320
321    private void printOutException(Exception ex) {
322        String top = "> > > > > >                                                         < < < < < <";
323        String exName = ex.getClass().getSimpleName();
324        StringBuilder sb = new StringBuilder(top.substring(0, 40 - (exName.length() / 2))).append(exName);
325        sb.append(top.substring(sb.length()));
326
327        System.out.println(sb);
328        if ((ex instanceof SchemaException) && (null != ((SchemaException) ex).getJsonValue())) {
329            System.out.append("Path: ").println(((SchemaException) ex).getJsonValue().getPointer().toString());
330        }
331        System.out.append("Message: ").println(ex.getMessage());
332        System.out.println("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
333
334    }
335
336    private Main() {
337
338    }
339}