001/* 002 * ModeShape (http://www.modeshape.org) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.modeshape.sequencer.ddl; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026import java.util.Set; 027import org.modeshape.common.annotation.Immutable; 028import org.modeshape.common.text.ParsingException; 029import org.modeshape.common.text.Position; 030import org.modeshape.common.util.CheckArg; 031import org.modeshape.jcr.api.JcrConstants; 032import org.modeshape.sequencer.ddl.dialect.derby.DerbyDdlParser; 033import org.modeshape.sequencer.ddl.dialect.oracle.OracleDdlParser; 034import org.modeshape.sequencer.ddl.dialect.postgres.PostgresDdlParser; 035import org.modeshape.sequencer.ddl.node.AstNode; 036import org.modeshape.sequencer.ddl.node.AstNodeFactory; 037 038/** 039 * A set of parsers capable of understanding DDL file content. This class can be used directly to create an {@link AstNode} tree 040 * representing nodes and properties for DDL statement components. 041 * <p> 042 * You can also provide an input or parent {@link AstNode} node as the starting point for your tree. 043 * </p> 044 * <p> 045 * The parser is based on the SQL-92 and extended by specific dialects. These dialect-specific parsers provide db-specific parsing 046 * of db-specific statements of statement extensions, features or properties. 047 * </p> 048 */ 049@Immutable 050public class DdlParsers { 051 052 /** 053 * Sorts the parser scores. 054 */ 055 private static final Comparator<Entry<DdlParser, Integer>> SORTER = new Comparator<Entry<DdlParser, Integer>>() { 056 057 @Override 058 public int compare( final Entry<DdlParser, Integer> thisEntry, 059 final Entry<DdlParser, Integer> thatEntry ) { 060 // reverse order as we want biggest value to sort first 061 int result = (thisEntry.getValue().compareTo(thatEntry.getValue()) * -1); 062 063 // default to standard SQL parser if score is a tie 064 if (result == 0) { 065 if (StandardDdlParser.ID.equals(thisEntry.getKey().getId()) 066 && !StandardDdlParser.ID.equals(thatEntry.getKey().getId())) { 067 return -1; 068 } 069 070 if (StandardDdlParser.ID.equals(thatEntry.getKey().getId()) 071 && !StandardDdlParser.ID.equals(thisEntry.getKey().getId())) { 072 return 1; 073 } 074 } 075 076 return result; 077 } 078 079 }; 080 081 public static final List<DdlParser> BUILTIN_PARSERS; 082 083 static { 084 List<DdlParser> parsers = new ArrayList<DdlParser>(); 085 parsers.add(new StandardDdlParser()); 086 parsers.add(new OracleDdlParser()); 087 parsers.add(new DerbyDdlParser()); 088 parsers.add(new PostgresDdlParser()); 089 BUILTIN_PARSERS = Collections.unmodifiableList(parsers); 090 } 091 092 private List<DdlParser> parsers; 093 private AstNodeFactory nodeFactory = new AstNodeFactory(); 094 095 /** 096 * Create an instance that uses all of the {@link #BUILTIN_PARSERS built-in parsers}. 097 */ 098 public DdlParsers() { 099 this.parsers = BUILTIN_PARSERS; 100 } 101 102 /** 103 * Create an instance that uses the supplied parsers, in order. 104 * 105 * @param parsers the list of parsers; may be empty or null if the {@link #BUILTIN_PARSERS built-in parsers} should be used 106 */ 107 public DdlParsers( List<DdlParser> parsers ) { 108 this.parsers = (parsers != null && !parsers.isEmpty()) ? parsers : BUILTIN_PARSERS; 109 } 110 111 private AstNode createDdlStatementsContainer( final String parserId ) { 112 final AstNode node = this.nodeFactory.node(StandardDdlLexicon.STATEMENTS_CONTAINER); 113 node.setProperty(JcrConstants.JCR_PRIMARY_TYPE, JcrConstants.NT_UNSTRUCTURED); 114 node.setProperty(StandardDdlLexicon.PARSER_ID, parserId); 115 return node; 116 } 117 118 /** 119 * @param id the identifier of the parser being requested (cannot be <code>null</code> or empty) 120 * @return the parser or <code>null</code> if not found 121 */ 122 public DdlParser getParser( final String id ) { 123 CheckArg.isNotEmpty(id, "id"); 124 125 for (final DdlParser parser : this.parsers) { 126 if (parser.getId().equals(id)) { 127 return parser; 128 } 129 } 130 131 return null; 132 } 133 134 /** 135 * @return a copy of the DDL parsers used in this instance (never <code>null</code> or empty) 136 */ 137 public Set<DdlParser> getParsers() { 138 return new HashSet<DdlParser>(this.parsers); 139 } 140 141 /** 142 * @param ddl the DDL being parsed (cannot be <code>null</code> or empty) 143 * @param parserId the identifier of the parser to use (can be <code>null</code> or empty if best matched parser should be 144 * used) 145 * @return the root tree {@link AstNode} 146 * @throws ParsingException if there is an error parsing the supplied DDL content 147 * @throws IllegalArgumentException if a parser with the specified identifier cannot be found 148 */ 149 public AstNode parseUsing( final String ddl, 150 final String parserId ) throws ParsingException { 151 CheckArg.isNotEmpty(ddl, "ddl"); 152 CheckArg.isNotEmpty(parserId, "parserId"); 153 154 DdlParser parser = getParser(parserId); 155 156 if (parser == null) { 157 throw new ParsingException(Position.EMPTY_CONTENT_POSITION, DdlSequencerI18n.unknownParser.text(parserId)); 158 } 159 160 // create DDL root node 161 AstNode astRoot = createDdlStatementsContainer(parserId); 162 163 // parse 164 parser.parse(ddl, astRoot, null); 165 166 return astRoot; 167 } 168 169 /** 170 * Parse the supplied DDL using multiple parsers, returning the result of each parser with its score in the order of highest 171 * scoring to lowest scoring. 172 * 173 * @param ddl the DDL being parsed (cannot be <code>null</code> or empty) 174 * @param firstParserId the identifier of the first parser to use (cannot be <code>null</code> or empty) 175 * @param secondParserId the identifier of the second parser to use (cannot be <code>null</code> or empty) 176 * @param additionalParserIds the identifiers of additional parsers that should be used; may be empty but not contain a null 177 * identifier value 178 * @return the list of {@link ParsingResult} instances, one for each parser, ordered from highest score to lowest score (never 179 * <code>null</code> or empty) 180 * @throws ParsingException if there is an error parsing the supplied DDL content 181 * @throws IllegalArgumentException if a parser with the specified identifier cannot be found 182 */ 183 public List<ParsingResult> parseUsing( final String ddl, 184 final String firstParserId, 185 final String secondParserId, 186 final String... additionalParserIds ) throws ParsingException { 187 CheckArg.isNotEmpty(firstParserId, "firstParserId"); 188 CheckArg.isNotEmpty(secondParserId, "secondParserId"); 189 190 if (additionalParserIds != null) { 191 CheckArg.containsNoNulls(additionalParserIds, "additionalParserIds"); 192 } 193 194 final int numParsers = ((additionalParserIds == null) ? 2 : (additionalParserIds.length + 2)); 195 final List<DdlParser> selectedParsers = new ArrayList<DdlParser>(numParsers); 196 197 { // add first parser 198 final DdlParser parser = getParser(firstParserId); 199 200 if (parser == null) { 201 throw new ParsingException(Position.EMPTY_CONTENT_POSITION, DdlSequencerI18n.unknownParser.text(firstParserId)); 202 } 203 204 selectedParsers.add(parser); 205 } 206 207 { // add second parser 208 final DdlParser parser = getParser(secondParserId); 209 210 if (parser == null) { 211 throw new ParsingException(Position.EMPTY_CONTENT_POSITION, DdlSequencerI18n.unknownParser.text(secondParserId)); 212 } 213 214 selectedParsers.add(parser); 215 } 216 217 // add remaining parsers 218 if ((additionalParserIds != null) && (additionalParserIds.length != 0)) { 219 for (final String id : additionalParserIds) { 220 final DdlParser parser = getParser(id); 221 222 if (parser == null) { 223 throw new ParsingException(Position.EMPTY_CONTENT_POSITION, DdlSequencerI18n.unknownParser.text(id)); 224 } 225 226 selectedParsers.add(parser); 227 } 228 } 229 230 return parseUsing(ddl, selectedParsers); 231 } 232 233 private List<ParsingResult> parseUsing( final String ddl, 234 final List<DdlParser> parsers ) { 235 CheckArg.isNotEmpty(ddl, "ddl"); 236 237 final List<ParsingResult> results = new ArrayList<DdlParsers.ParsingResult>(this.parsers.size()); 238 final DdlParserScorer scorer = new DdlParserScorer(); 239 240 for (final DdlParser parser : this.parsers) { 241 final String parserId = parser.getId(); 242 int score = ParsingResult.NO_SCORE; 243 AstNode rootNode = null; 244 Exception error = null; 245 246 try { 247 // score 248 final Object scorerOutput = parser.score(ddl, null, scorer); 249 score = scorer.getScore(); 250 251 // create DDL root node 252 rootNode = createDdlStatementsContainer(parserId); 253 254 // parse 255 parser.parse(ddl, rootNode, scorerOutput); 256 } catch (final RuntimeException e) { 257 error = e; 258 } finally { 259 final ParsingResult result = new ParsingResult(parserId, rootNode, score, error); 260 results.add(result); 261 scorer.reset(); 262 } 263 } 264 265 Collections.sort(results); 266 return results; 267 } 268 269 /** 270 * Parse the supplied DDL using all registered parsers, returning the result of each parser with its score in the order of 271 * highest scoring to lowest scoring. 272 * 273 * @param ddl the DDL being parsed (cannot be <code>null</code> or empty) 274 * @return the list or {@link ParsingResult} instances, one for each parser, ordered from highest score to lowest score 275 * @throws ParsingException if there is an error parsing the supplied DDL content 276 * @throws IllegalArgumentException if a parser with the specified identifier cannot be found 277 */ 278 public List<ParsingResult> parseUsingAll( final String ddl ) throws ParsingException { 279 return parseUsing(ddl, this.parsers); 280 } 281 282 /** 283 * Parse the supplied DDL content and return the {@link AstNode root node} of the AST representation. 284 * 285 * @param ddl content string; may not be null 286 * @param fileName the approximate name of the file containing the DDL content; may be null if this is not known 287 * @return the root tree {@link AstNode} 288 * @throws ParsingException if there is an error parsing the supplied DDL content 289 */ 290 public AstNode parse( final String ddl, 291 final String fileName ) throws ParsingException { 292 CheckArg.isNotEmpty(ddl, "ddl"); 293 RuntimeException firstException = null; 294 295 // Go through each parser and score the DDL content 296 final Map<DdlParser, Integer> scoreMap = new HashMap<DdlParser, Integer>(this.parsers.size()); 297 final DdlParserScorer scorer = new DdlParserScorer(); 298 299 for (final DdlParser parser : this.parsers) { 300 try { 301 parser.score(ddl, fileName, scorer); 302 scoreMap.put(parser, scorer.getScore()); 303 } catch (RuntimeException e) { 304 if (firstException == null) { 305 firstException = e; 306 } 307 } finally { 308 scorer.reset(); 309 } 310 } 311 312 if (scoreMap.isEmpty()) { 313 if (firstException == null) { 314 throw new ParsingException(Position.EMPTY_CONTENT_POSITION, 315 DdlSequencerI18n.errorParsingDdlContent.text(this.parsers.size())); 316 } 317 318 throw firstException; 319 } 320 321 // sort the scores 322 final List<Entry<DdlParser, Integer>> scoredParsers = new ArrayList<Entry<DdlParser, Integer>>(scoreMap.entrySet()); 323 Collections.sort(scoredParsers, SORTER); 324 325 firstException = null; 326 AstNode astRoot = null; 327 328 for (final Entry<DdlParser, Integer> scoredParser : scoredParsers) { 329 try { 330 final DdlParser parser = scoredParser.getKey(); 331 332 // create DDL root node 333 astRoot = createDdlStatementsContainer(parser.getId()); 334 335 // parse 336 parser.parse(ddl, astRoot, null); 337 return astRoot; // successfully parsed 338 } catch (final RuntimeException e) { 339 if (astRoot != null) { 340 astRoot.removeFromParent(); 341 } 342 343 if (firstException == null) { 344 firstException = e; 345 } 346 } 347 } 348 349 if (firstException == null) { 350 throw new ParsingException(Position.EMPTY_CONTENT_POSITION, DdlSequencerI18n.errorParsingDdlContent.text()); 351 } 352 353 throw firstException; 354 } 355 356 /** 357 * Represents a parsing result of one parser parsing one DDL input. 358 */ 359 @Immutable 360 public class ParsingResult implements Comparable<ParsingResult> { 361 362 public static final int NO_SCORE = -1; 363 364 private final Exception error; 365 private final String id; 366 private final AstNode rootNode; 367 private final int score; 368 369 /** 370 * @param parserId the parser identifier (cannot be <code>null</code> or empty) 371 * @param rootTreeNode the node at the root of the parse tree (can be <code>null</code> if an error occurred) 372 * @param parserScore the parsing score (can have {@link #NO_SCORE no score} if an error occurred 373 * @param parsingError an error that occurred during parsing (can be <code>null</code>) 374 */ 375 public ParsingResult( final String parserId, 376 final AstNode rootTreeNode, 377 final int parserScore, 378 final Exception parsingError ) { 379 CheckArg.isNotEmpty(parserId, "parserId"); 380 381 this.id = parserId; 382 this.rootNode = rootTreeNode; 383 this.score = parserScore; 384 this.error = parsingError; 385 } 386 387 /** 388 * {@inheritDoc} 389 * 390 * @see java.lang.Comparable#compareTo(java.lang.Object) 391 */ 392 @Override 393 public int compareTo( final ParsingResult that ) { 394 if ((this == that) || (this.score == that.score)) { 395 return 0; 396 } 397 398 return ((this.score > that.score) ? -1 : 1); 399 } 400 401 /** 402 * @return the parsing error (<code>null</code> if no error occurred) 403 */ 404 public Exception getError() { 405 return this.error; 406 } 407 408 /** 409 * @return the parser identifier (never <code>null</code> or empty) 410 */ 411 public String getParserId() { 412 return this.id; 413 } 414 415 /** 416 * @return the root <code>AstNode</code> (can be <code>null</code> if a parsing error occurred) 417 */ 418 public AstNode getRootTree() { 419 return this.rootNode; 420 } 421 422 /** 423 * @return the parsing score 424 */ 425 public int getScore() { 426 return this.score; 427 } 428 429 } 430 431}