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}