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}