View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.graph.query.validate;
25  
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  import net.jcip.annotations.Immutable;
34  import net.jcip.annotations.NotThreadSafe;
35  import org.modeshape.common.text.ParsingException;
36  import org.modeshape.common.util.CheckArg;
37  import org.modeshape.graph.GraphI18n;
38  import org.modeshape.graph.query.QueryContext;
39  import org.modeshape.graph.query.model.QueryCommand;
40  import org.modeshape.graph.query.model.SelectorName;
41  import org.modeshape.graph.query.model.TypeSystem;
42  import org.modeshape.graph.query.model.Visitors;
43  import org.modeshape.graph.query.parse.InvalidQueryException;
44  import org.modeshape.graph.query.parse.SqlQueryParser;
45  import org.modeshape.graph.query.plan.CanonicalPlanner;
46  import org.modeshape.graph.query.plan.PlanHints;
47  import org.modeshape.graph.query.plan.PlanNode;
48  import org.modeshape.graph.query.plan.PlanNode.Property;
49  import org.modeshape.graph.query.plan.PlanNode.Type;
50  
51  /**
52   * An immutable {@link Schemata} implementation.
53   */
54  @Immutable
55  public class ImmutableSchemata implements Schemata {
56  
57      /**
58       * Obtain a new instance for building Schemata objects.
59       * 
60       * @param typeSystem the type system that this schemata should use
61       * @return the new builder; never null
62       * @throws IllegalArgumentException if the context is null
63       */
64      public static Builder createBuilder( TypeSystem typeSystem ) {
65          CheckArg.isNotNull(typeSystem, "typeSystem");
66          return new Builder(typeSystem);
67      }
68  
69      /**
70       * A builder of immutable {@link Schemata} objects.
71       */
72      @NotThreadSafe
73      public static class Builder {
74  
75          private final TypeSystem typeSystem;
76          private final Map<SelectorName, ImmutableTable> tables = new HashMap<SelectorName, ImmutableTable>();
77          private final Map<SelectorName, QueryCommand> viewDefinitions = new HashMap<SelectorName, QueryCommand>();
78          private final Set<SelectorName> tablesOrViewsWithExtraColumns = new HashSet<SelectorName>();
79  
80          protected Builder( TypeSystem typeSystem ) {
81              this.typeSystem = typeSystem;
82          }
83  
84          /**
85           * Add a table with the supplied name and column names. Each column will be given a default type. The table will also
86           * overwrite any existing table definition with the same name.
87           * 
88           * @param name the name of the new table
89           * @param columnNames the names of the columns.
90           * @return this builder, for convenience in method chaining; never null
91           * @throws IllegalArgumentException if the table name is null or empty, any column name is null or empty, or if no column
92           *         names are given
93           */
94          public Builder addTable( String name,
95                                   String... columnNames ) {
96              CheckArg.isNotEmpty(name, "name");
97              CheckArg.isNotEmpty(columnNames, "columnNames");
98              List<Column> columns = new ArrayList<Column>();
99              int i = 0;
100             for (String columnName : columnNames) {
101                 CheckArg.isNotEmpty(columnName, "columnName[" + (i++) + "]");
102                 columns.add(new ImmutableColumn(columnName, typeSystem.getDefaultType()));
103             }
104             ImmutableTable table = new ImmutableTable(new SelectorName(name), columns, false);
105             tables.put(table.getName(), table);
106             return this;
107         }
108 
109         /**
110          * Add a table with the supplied name and column names and types. The table will also overwrite any existing table
111          * definition with the same name.
112          * 
113          * @param name the name of the new table
114          * @param columnNames the names of the columns
115          * @param types the types for the columns
116          * @return this builder, for convenience in method chaining; never null
117          * @throws IllegalArgumentException if the table name is null or empty, any column name is null or empty, if no column
118          *         names are given, or if the number of types does not match the number of columns
119          */
120         public Builder addTable( String name,
121                                  String[] columnNames,
122                                  String[] types ) {
123             CheckArg.isNotEmpty(name, "name");
124             CheckArg.isNotEmpty(columnNames, "columnNames");
125             CheckArg.isNotEmpty(types, "types");
126             CheckArg.isEquals(columnNames.length, "columnNames.length", types.length, "types.length");
127             List<Column> columns = new ArrayList<Column>();
128             assert columnNames.length == types.length;
129             for (int i = 0; i != columnNames.length; ++i) {
130                 String columnName = columnNames[i];
131                 CheckArg.isNotEmpty(columnName, "columnName[" + i + "]");
132                 columns.add(new ImmutableColumn(columnName, types[i]));
133             }
134             ImmutableTable table = new ImmutableTable(new SelectorName(name), columns, false);
135             tables.put(table.getName(), table);
136             return this;
137         }
138 
139         /**
140          * Add a view with the supplied name and SQL string definition. The column names and types will be inferred from the
141          * source table(s) and views(s) used in the definition.
142          * 
143          * @param name the name of the new view
144          * @param definition the SQL definition of the view
145          * @return this builder, for convenience in method chaining; never null
146          * @throws IllegalArgumentException if the view name is null or empty or the definition is null
147          * @throws ParsingException if the supplied definition is cannot be parsed as a SQL query
148          */
149         public Builder addView( String name,
150                                 String definition ) {
151             CheckArg.isNotEmpty(name, "name");
152             CheckArg.isNotEmpty(definition, "definition");
153             SqlQueryParser parser = new SqlQueryParser();
154             QueryCommand command = parser.parseQuery(definition, typeSystem);
155             this.viewDefinitions.put(new SelectorName(name), command);
156             return this;
157         }
158 
159         /**
160          * Add a view with the supplied name and definition. The column names and types will be inferred from the source table(s)
161          * used in the definition.
162          * 
163          * @param name the name of the new view
164          * @param definition the definition of the view
165          * @return this builder, for convenience in method chaining; never null
166          * @throws IllegalArgumentException if the view name is null or empty or the definition is null
167          */
168         public Builder addView( String name,
169                                 QueryCommand definition ) {
170             CheckArg.isNotEmpty(name, "name");
171             CheckArg.isNotNull(definition, "definition");
172             this.viewDefinitions.put(new SelectorName(name), definition);
173             return this;
174         }
175 
176         /**
177          * Add a column with the supplied name and type to the named table. Any existing column with that name will be replaced
178          * with the new column. If the table does not yet exist, it will be added.
179          * 
180          * @param tableName the name of the new table
181          * @param columnName the names of the column
182          * @param type the type for the column
183          * @return this builder, for convenience in method chaining; never null
184          * @throws IllegalArgumentException if the table name is null or empty, any column name is null or empty, if no column
185          *         names are given, or if the number of types does not match the number of columns
186          */
187         public Builder addColumn( String tableName,
188                                   String columnName,
189                                   String type ) {
190             CheckArg.isNotEmpty(tableName, "tableName");
191             CheckArg.isNotEmpty(columnName, "columnName");
192             CheckArg.isNotNull(type, "type");
193             return addColumn(tableName, columnName, type, ImmutableColumn.DEFAULT_FULL_TEXT_SEARCHABLE);
194         }
195 
196         /**
197          * Add a column with the supplied name and type to the named table. Any existing column with that name will be replaced
198          * with the new column. If the table does not yet exist, it will be added.
199          * 
200          * @param tableName the name of the new table
201          * @param columnName the names of the column
202          * @param type the type for the column
203          * @param fullTextSearchable true if the column should be full-text searchable, or false if not
204          * @return this builder, for convenience in method chaining; never null
205          * @throws IllegalArgumentException if the table name is null or empty, the column name is null or empty, or if the
206          *         property type is null
207          */
208         public Builder addColumn( String tableName,
209                                   String columnName,
210                                   String type,
211                                   boolean fullTextSearchable ) {
212             CheckArg.isNotEmpty(tableName, "tableName");
213             CheckArg.isNotEmpty(columnName, "columnName");
214             CheckArg.isNotNull(type, "type");
215             SelectorName selector = new SelectorName(tableName);
216             ImmutableTable existing = tables.get(selector);
217             ImmutableTable table = null;
218             if (existing == null) {
219                 List<Column> columns = new ArrayList<Column>();
220                 columns.add(new ImmutableColumn(columnName, type, fullTextSearchable));
221                 table = new ImmutableTable(selector, columns, false);
222             } else {
223                 table = existing.withColumn(columnName, type, fullTextSearchable);
224             }
225             tables.put(table.getName(), table);
226             return this;
227         }
228 
229         /**
230          * Make sure the column on the named table is searchable.
231          * 
232          * @param tableName the name of the new table
233          * @param columnName the names of the column
234          * @return this builder, for convenience in method chaining; never null
235          * @throws IllegalArgumentException if the table name is null or empty or if the column name is null or empty
236          */
237         public Builder makeSearchable( String tableName,
238                                        String columnName ) {
239             CheckArg.isNotEmpty(tableName, "tableName");
240             CheckArg.isNotEmpty(columnName, "columnName");
241             SelectorName selector = new SelectorName(tableName);
242             ImmutableTable existing = tables.get(selector);
243             ImmutableTable table = null;
244             if (existing == null) {
245                 List<Column> columns = new ArrayList<Column>();
246                 columns.add(new ImmutableColumn(columnName, typeSystem.getDefaultType(), true));
247                 table = new ImmutableTable(selector, columns, false);
248             } else {
249                 Column column = existing.getColumn(columnName);
250                 String type = typeSystem.getDefaultType();
251                 if (column != null) {
252                     type = column.getPropertyType();
253                 }
254                 table = existing.withColumn(columnName, type, true);
255             }
256             tables.put(table.getName(), table);
257             return this;
258         }
259 
260         /**
261          * Make sure the column on the named table has extra columns that can be used without validation error.
262          * 
263          * @param tableName the name of the table
264          * @return this builder, for convenience in method chaining; never null
265          * @throws IllegalArgumentException if the table name does is null or empty
266          */
267         public Builder markExtraColumns( String tableName ) {
268             CheckArg.isNotEmpty(tableName, "tableName");
269             SelectorName selector = new SelectorName(tableName);
270             tablesOrViewsWithExtraColumns.add(selector);
271             return this;
272         }
273 
274         /**
275          * Add to the specified table a key that references the existing named columns.
276          * 
277          * @param tableName the name of the new table
278          * @param columnNames the names of the (existing) columns that make up the key
279          * @return this builder, for convenience in method chaining; never null
280          * @throws IllegalArgumentException if the table name is null or empty, the array of column names is null or empty, or if
281          *         the column names do not reference existing columns in the table
282          */
283         public Builder addKey( String tableName,
284                                String... columnNames ) {
285             CheckArg.isNotEmpty(tableName, "tableName");
286             CheckArg.isNotEmpty(columnNames, "columnNames");
287             ImmutableTable existing = tables.get(new SelectorName(tableName));
288             if (existing == null) {
289                 throw new IllegalArgumentException(GraphI18n.tableDoesNotExist.text(tableName));
290             }
291             Set<Column> keyColumns = new HashSet<Column>();
292             for (String columnName : columnNames) {
293                 Column existingColumn = existing.getColumnsByName().get(columnName);
294                 if (existingColumn == null) {
295                     String msg = GraphI18n.schemataKeyReferencesNonExistingColumn.text(tableName, columnName);
296                     throw new IllegalArgumentException(msg);
297                 }
298                 keyColumns.add(existingColumn);
299             }
300             ImmutableTable table = existing.withKey(keyColumns);
301             tables.put(table.getName(), table);
302             return this;
303         }
304 
305         /**
306          * Build the {@link Schemata} instance, using the current state of the builder. This method creates a snapshot of the
307          * tables (with their columns) as they exist at the moment this method is called.
308          * 
309          * @return the new Schemata; never null
310          * @throws InvalidQueryException if any of the view definitions is invalid and cannot be resolved
311          */
312         public Schemata build() {
313             // Go through the tables and mark those that have extra columns ...
314             for (SelectorName tableName : tablesOrViewsWithExtraColumns) {
315                 ImmutableTable table = tables.get(tableName);
316                 if (table != null) {
317                     tables.put(table.getName(), table.withExtraColumns());
318                 }
319             }
320 
321             ImmutableSchemata schemata = new ImmutableSchemata(new HashMap<SelectorName, Table>(tables));
322 
323             // Make a copy of the view definitions, and create the views ...
324             Map<SelectorName, QueryCommand> definitions = new HashMap<SelectorName, QueryCommand>(viewDefinitions);
325             boolean added = false;
326             do {
327                 added = false;
328                 Set<SelectorName> viewNames = new HashSet<SelectorName>(definitions.keySet());
329                 for (SelectorName name : viewNames) {
330                     QueryCommand command = definitions.get(name);
331                     // Create the canonical plan for the definition ...
332                     PlanHints hints = new PlanHints();
333                     hints.validateColumnExistance = false;
334                     QueryContext queryContext = new QueryContext(schemata, typeSystem, hints);
335                     CanonicalPlanner planner = new CanonicalPlanner();
336                     PlanNode plan = planner.createPlan(queryContext, command);
337                     if (queryContext.getProblems().hasErrors()) {
338                         continue;
339                     }
340 
341                     // Get the columns from the top-level PROJECT ...
342                     PlanNode project = plan.findAtOrBelow(Type.PROJECT);
343                     assert project != null;
344                     List<org.modeshape.graph.query.model.Column> columns = project.getPropertyAsList(Property.PROJECT_COLUMNS,
345                                                                                                      org.modeshape.graph.query.model.Column.class);
346                     assert !columns.isEmpty();
347 
348                     // Go through all the columns and look up the types ...
349                     Map<SelectorName, SelectorName> tableNameByAlias = null;
350                     List<Column> viewColumns = new ArrayList<Column>(columns.size());
351                     for (org.modeshape.graph.query.model.Column column : columns) {
352                         // Find the table that the column came from ...
353                         Table source = schemata.getTable(column.selectorName());
354                         if (source == null) {
355                             // The column may be referring to the alias of the table ...
356                             if (tableNameByAlias == null) {
357                                 tableNameByAlias = Visitors.getSelectorNamesByAlias(command);
358                             }
359                             SelectorName tableName = tableNameByAlias.get(column.selectorName());
360                             if (tableName != null) source = schemata.getTable(tableName);
361                             if (source == null) {
362                                 continue;
363                             }
364                         }
365                         String viewColumnName = column.columnName();
366                         String sourceColumnName = column.propertyName(); // getColumnName() returns alias
367                         Column sourceColumn = source.getColumn(sourceColumnName);
368                         if (sourceColumn == null) {
369                             throw new InvalidQueryException(Visitors.readable(command),
370                                                             "The view references a non-existant column '" + column.columnName()
371                                                             + "' in '" + source.getName() + "'");
372                         }
373                         viewColumns.add(new ImmutableColumn(viewColumnName, sourceColumn.getPropertyType(),
374                                                             sourceColumn.isFullTextSearchable()));
375                     }
376                     if (viewColumns.size() != columns.size()) {
377                         // We weren't able to resolve all of the columns,
378                         // so maybe the columns were referencing yet-to-be-built views ...
379                         continue;
380                     }
381 
382                     // If we could resolve the definition ...
383                     boolean hasExtraColumns = tablesOrViewsWithExtraColumns.contains(name);
384                     ImmutableView view = new ImmutableView(name, viewColumns, hasExtraColumns, command);
385                     definitions.remove(name);
386                     schemata = schemata.with(view);
387                     added = true;
388                 }
389             } while (added && !definitions.isEmpty());
390 
391             if (!definitions.isEmpty()) {
392                 QueryCommand command = definitions.values().iterator().next();
393                 throw new InvalidQueryException(Visitors.readable(command), "The view definition cannot be resolved: "
394                                                                             + Visitors.readable(command));
395             }
396 
397             return schemata;
398         }
399     }
400 
401     private final Map<SelectorName, Table> tables;
402 
403     protected ImmutableSchemata( Map<SelectorName, Table> tables ) {
404         this.tables = Collections.unmodifiableMap(tables);
405     }
406 
407     /**
408      * {@inheritDoc}
409      * 
410      * @see org.modeshape.graph.query.validate.Schemata#getTable(org.modeshape.graph.query.model.SelectorName)
411      */
412     public Table getTable( SelectorName name ) {
413         return tables.get(name);
414     }
415 
416     public ImmutableSchemata with( Table table ) {
417         Map<SelectorName, Table> tables = new HashMap<SelectorName, Table>(this.tables);
418         tables.put(table.getName(), table);
419         return new ImmutableSchemata(tables);
420     }
421 
422     /**
423      * {@inheritDoc}
424      * 
425      * @see java.lang.Object#toString()
426      */
427     @Override
428     public String toString() {
429         StringBuilder sb = new StringBuilder();
430         boolean first = true;
431         for (Table table : tables.values()) {
432             if (first) first = false;
433             else sb.append('\n');
434             sb.append(table);
435         }
436         return sb.toString();
437     }
438 
439 }