Hide TyperUtils.extractRowUpdates()
[ovsdb.git] / library / impl / src / main / java / org / opendaylight / ovsdb / lib / schema / typed / TyperUtils.java
1 /*
2  * Copyright © 2014, 2017 Red Hat, Inc. and others. All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8
9 package org.opendaylight.ovsdb.lib.schema.typed;
10
11 import com.google.common.annotations.VisibleForTesting;
12 import com.google.common.base.Objects;
13 import com.google.common.base.Preconditions;
14 import com.google.common.collect.Range;
15 import com.google.common.reflect.Reflection;
16 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
17 import java.lang.reflect.InvocationHandler;
18 import java.lang.reflect.Method;
19 import java.util.HashMap;
20 import java.util.Locale;
21 import java.util.Map;
22 import org.opendaylight.ovsdb.lib.error.ColumnSchemaNotFoundException;
23 import org.opendaylight.ovsdb.lib.error.SchemaVersionMismatchException;
24 import org.opendaylight.ovsdb.lib.error.TableSchemaNotFoundException;
25 import org.opendaylight.ovsdb.lib.error.TyperException;
26 import org.opendaylight.ovsdb.lib.error.UnsupportedMethodException;
27 import org.opendaylight.ovsdb.lib.message.TableUpdate;
28 import org.opendaylight.ovsdb.lib.message.TableUpdates;
29 import org.opendaylight.ovsdb.lib.notation.Column;
30 import org.opendaylight.ovsdb.lib.notation.Row;
31 import org.opendaylight.ovsdb.lib.notation.UUID;
32 import org.opendaylight.ovsdb.lib.notation.Version;
33 import org.opendaylight.ovsdb.lib.schema.ColumnSchema;
34 import org.opendaylight.ovsdb.lib.schema.DatabaseSchema;
35 import org.opendaylight.ovsdb.lib.schema.GenericTableSchema;
36 import org.opendaylight.ovsdb.lib.schema.TableSchema;
37
38 /**
39  * Utility methods for typed OVSDB schema data.
40  */
41 public final class TyperUtils {
42
43     private static final String GET_STARTS_WITH = "get";
44     private static final String SET_STARTS_WITH = "set";
45     private static final String GETCOLUMN_ENDS_WITH = "Column";
46     private static final String GETROW_ENDS_WITH = "Row";
47
48     private TyperUtils() {
49         // Prevent instantiating a utility class
50     }
51
52     /**
53      * Retrieve the table schema for the given table in the given database schema.
54      *
55      * @param dbSchema The database schema.
56      * @param klazz The class whose table schema should be retrieved. Classes are matched in the database schema either
57      *     using their {@link TypedTable} annotation, if they have one, or by name.
58      * @return the table schema.
59      */
60     public static GenericTableSchema getTableSchema(final DatabaseSchema dbSchema, final Class<?> klazz) {
61         return dbSchema.table(TypedReflections.getTableName(klazz), GenericTableSchema.class);
62     }
63
64     public static ColumnSchema<GenericTableSchema, Object> getColumnSchema(final GenericTableSchema tableSchema,
65             final String columnName, final Class<Object> metaClass) {
66         return tableSchema.column(columnName, metaClass);
67     }
68
69     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
70             justification = "https://github.com/spotbugs/spotbugs/issues/811")
71     private static String getColumnName(final Method method) {
72         TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
73         if (typedColumn != null) {
74             return typedColumn.name();
75         }
76
77         /*
78          * Attempting to get the column name by parsing the method name with a following convention :
79          * 1. GETDATA : get<ColumnName>
80          * 2. SETDATA : set<ColumnName>
81          * 3. GETCOLUMN : get<ColumnName>Column
82          * where <ColumnName> is the name of the column that we are interested in.
83          */
84         int index = GET_STARTS_WITH.length();
85         if (isGetData(method) || isSetData(method)) {
86             return method.getName().substring(index, method.getName().length()).toLowerCase(Locale.ROOT);
87         } else if (isGetColumn(method)) {
88             return method.getName().substring(index, method.getName().indexOf(GETCOLUMN_ENDS_WITH,
89                     index)).toLowerCase(Locale.ROOT);
90         }
91
92         return null;
93     }
94
95     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
96             justification = "https://github.com/spotbugs/spotbugs/issues/811")
97     private static boolean isGetTableSchema(final Method method) {
98         TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
99         return typedColumn != null && typedColumn.method().equals(MethodType.GETTABLESCHEMA);
100     }
101
102     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
103             justification = "https://github.com/spotbugs/spotbugs/issues/811")
104     private static boolean isGetRow(final Method method) {
105         TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
106         if (typedColumn != null) {
107             return typedColumn.method().equals(MethodType.GETROW);
108         }
109
110         return method.getName().startsWith(GET_STARTS_WITH) && method.getName().endsWith(GETROW_ENDS_WITH);
111     }
112
113     private static boolean isGetColumn(final Method method) {
114         TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
115         if (typedColumn != null) {
116             return typedColumn.method().equals(MethodType.GETCOLUMN);
117         }
118
119         return method.getName().startsWith(GET_STARTS_WITH) && method.getName().endsWith(GETCOLUMN_ENDS_WITH);
120     }
121
122     private static boolean isGetData(final Method method) {
123         TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
124         if (typedColumn != null) {
125             return typedColumn.method().equals(MethodType.GETDATA);
126         }
127
128         return method.getName().startsWith(GET_STARTS_WITH) && !method.getName().endsWith(GETCOLUMN_ENDS_WITH);
129     }
130
131     private static boolean isSetData(final Method method) {
132         TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
133         if (typedColumn != null) {
134             return typedColumn.method().equals(MethodType.SETDATA);
135         }
136
137         return method.getName().startsWith(SET_STARTS_WITH);
138     }
139
140     /**
141      * Method that checks validity of the parameter passed to getTypedRowWrapper.
142      * This method checks for a valid Database Schema matching the expected Database for a given table
143      * and checks for the presence of the Table in Database Schema.
144      *
145      * @param dbSchema DatabaseSchema as learnt from a OVSDB connection
146      * @param klazz Typed Class that represents a Table
147      * @return true if valid, false otherwise
148      */
149     private static <T> boolean isValid(final DatabaseSchema dbSchema, final Class<T> klazz) {
150         if (dbSchema == null) {
151             return false;
152         }
153
154         final String dbName = TypedReflections.getTableDatabase(klazz);
155         if (dbName != null && !dbSchema.getName().equalsIgnoreCase(dbName)) {
156             return false;
157         }
158
159         checkTableSchemaVersion(dbSchema, klazz);
160
161         return true;
162     }
163
164     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
165             justification = "https://github.com/spotbugs/spotbugs/issues/811")
166     private static void checkColumnSchemaVersion(final DatabaseSchema dbSchema, final Method method) {
167         checkVersion(dbSchema.getVersion(), TypedReflections.getColumnVersionRange(method));
168     }
169
170     private static <T> void checkTableSchemaVersion(final DatabaseSchema dbSchema, final Class<T> klazz) {
171         checkVersion(dbSchema.getVersion(), TypedReflections.getTableVersionRange(klazz));
172     }
173
174     @VisibleForTesting
175     static void checkVersion(final Version schemaVersion, final Range<Version> range) {
176         if (!range.contains(schemaVersion)) {
177             throw new SchemaVersionMismatchException(schemaVersion,
178                 range.hasLowerBound() ? range.lowerEndpoint() : Version.NULL,
179                         range.hasUpperBound() ? range.upperEndpoint() : Version.NULL);
180         }
181     }
182
183     /**
184      * Returns a Typed Proxy implementation for the klazz passed as a parameter.
185      * Per design choice, the Typed Proxy implementation is just a Wrapper on top of the actual
186      * Row which is untyped.
187      *
188      * <p>Being just a wrapper, it is state-less and more of a convenience functionality to
189      * provide a type-safe infrastructure for the applications to built on top of.
190      * And this Typed infra is completely optional.
191      *
192      * <p>It is the applications responsibility to pass on the raw Row parameter and this method will
193      * return the appropriate Proxy wrapper for the passed klazz Type.
194      * The raw Row parameter may be null if the caller is interested in just the ColumnSchema.
195      * But that is not a very common use-case.
196      *
197      * @param dbSchema DatabaseSchema as learnt from a OVSDB connection
198      * @param klazz Typed Class that represents a Table
199      */
200     public static <T> T getTypedRowWrapper(final DatabaseSchema dbSchema, final Class<T> klazz) {
201         return getTypedRowWrapper(dbSchema, klazz, new Row<>());
202     }
203
204     /**
205      * Returns a Typed Proxy implementation for the klazz passed as a parameter.
206      * Per design choice, the Typed Proxy implementation is just a Wrapper on top of the actual
207      * Row which is untyped.
208      *
209      * <p>Being just a wrapper, it is state-less and more of a convenience functionality
210      * to provide a type-safe infrastructure for the applications to built on top of.
211      * And this Typed infra is completely optional.
212      *
213      * <p>It is the applications responsibility to pass on the raw Row parameter and this method
214      * will return the appropriate Proxy wrapper for the passed klazz Type.
215      * The raw Row parameter may be null if the caller is interested in just the
216      * ColumnSchema. But that is not a very common use-case.
217      *
218      * @param dbSchema DatabaseSchema as learnt from a OVSDB connection
219      * @param klazz Typed Class that represents a Table
220      * @param row The actual Row that the wrapper is operating on. It can be null if the caller
221      *            is just interested in getting ColumnSchema.
222      */
223     public static <T> T getTypedRowWrapper(final DatabaseSchema dbSchema, final Class<T> klazz,
224                                            final Row<GenericTableSchema> row) {
225         if (!isValid(dbSchema, klazz)) {
226             return null;
227         }
228         if (row != null) {
229             row.setTableSchema(getTableSchema(dbSchema, klazz));
230         }
231         return Reflection.newProxy(klazz, new InvocationHandler() {
232             private Object processGetData(final Method method) {
233                 String columnName = getColumnName(method);
234                 checkColumnSchemaVersion(dbSchema, method);
235                 if (columnName == null) {
236                     throw new TyperException("Error processing Getter : " + method.getName());
237                 }
238                 GenericTableSchema tableSchema = getTableSchema(dbSchema, klazz);
239                 if (tableSchema == null) {
240                     String message = TableSchemaNotFoundException.createMessage(TypedReflections.getTableName(klazz),
241                                 dbSchema.getName());
242                     throw new TableSchemaNotFoundException(message);
243                 }
244                 ColumnSchema<GenericTableSchema, Object> columnSchema =
245                         getColumnSchema(tableSchema, columnName, (Class<Object>) method.getReturnType());
246                 if (columnSchema == null) {
247                     String message = ColumnSchemaNotFoundException.createMessage(columnName, tableSchema.getName());
248                     throw new ColumnSchemaNotFoundException(message);
249                 }
250                 if (row == null || row.getColumn(columnSchema) == null) {
251                     return null;
252                 }
253                 return row.getColumn(columnSchema).getData();
254             }
255
256             private Object processGetRow() {
257                 return row;
258             }
259
260             private Object processGetColumn(final Method method) {
261                 String columnName = getColumnName(method);
262                 checkColumnSchemaVersion(dbSchema, method);
263                 if (columnName == null) {
264                     throw new TyperException("Error processing GetColumn : " + method.getName());
265                 }
266                 GenericTableSchema tableSchema = getTableSchema(dbSchema, klazz);
267                 if (tableSchema == null) {
268                     String message = TableSchemaNotFoundException.createMessage(TypedReflections.getTableName(klazz),
269                         dbSchema.getName());
270                     throw new TableSchemaNotFoundException(message);
271                 }
272                 ColumnSchema<GenericTableSchema, Object> columnSchema =
273                         getColumnSchema(tableSchema, columnName, (Class<Object>) method.getReturnType());
274                 if (columnSchema == null) {
275                     String message = ColumnSchemaNotFoundException.createMessage(columnName, tableSchema.getName());
276                     throw new ColumnSchemaNotFoundException(message);
277                 }
278                 // When the row is null, that might indicate that the user maybe interested
279                 // only in the ColumnSchema and not on the Data.
280                 if (row == null) {
281                     return new Column<>(columnSchema, null);
282                 }
283                 return row.getColumn(columnSchema);
284             }
285
286             private Object processSetData(final Object proxy, final Method method, final Object[] args) {
287                 if (args == null || args.length != 1) {
288                     throw new TyperException("Setter method : " + method.getName() + " requires 1 argument");
289                 }
290                 checkColumnSchemaVersion(dbSchema, method);
291                 String columnName = getColumnName(method);
292                 if (columnName == null) {
293                     throw new TyperException("Unable to locate Column Name for " + method.getName());
294                 }
295                 GenericTableSchema tableSchema = getTableSchema(dbSchema, klazz);
296                 ColumnSchema<GenericTableSchema, Object> columnSchema =
297                         getColumnSchema(tableSchema, columnName, (Class<Object>) args[0].getClass());
298                 Column<GenericTableSchema, Object> column =
299                         new Column<>(columnSchema, args[0]);
300                 row.addColumn(columnName, column);
301                 return proxy;
302             }
303
304             private Object processGetTableSchema() {
305                 if (dbSchema == null) {
306                     return null;
307                 }
308                 return getTableSchema(dbSchema, klazz);
309             }
310
311             private Boolean isHashCodeMethod(final Method method, final Object[] args) {
312                 return (args == null || args.length == 0) && method.getName().equals("hashCode");
313             }
314
315             private Boolean isEqualsMethod(final Method method, final Object[] args) {
316                 return args != null
317                         && args.length == 1
318                         && method.getName().equals("equals")
319                         && Object.class.equals(method.getParameterTypes()[0]);
320             }
321
322             private Boolean isToStringMethod(final Method method, final Object[] args) {
323                 return (args == null || args.length == 0) && method.getName().equals("toString");
324             }
325
326             @Override
327             public Object invoke(final Object proxy, final Method method, final Object[] args) throws Exception {
328                 if (isGetTableSchema(method)) {
329                     return processGetTableSchema();
330                 } else if (isGetRow(method)) {
331                     return processGetRow();
332                 } else if (isSetData(method)) {
333                     return processSetData(proxy, method, args);
334                 } else if (isGetData(method)) {
335                     return processGetData(method);
336                 } else if (isGetColumn(method)) {
337                     return processGetColumn(method);
338                 } else if (isHashCodeMethod(method, args)) {
339                     return hashCode();
340                 } else if (isEqualsMethod(method, args)) {
341                     return proxy.getClass().isInstance(args[0]) && this.equals(args[0]);
342                 } else if (isToStringMethod(method, args)) {
343                     return this.toString();
344                 }
345                 throw new UnsupportedMethodException("Method not supported " + method.toString());
346             }
347
348             @Override
349             @SuppressFBWarnings({"EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS", "EQ_UNUSUAL"})
350             public boolean equals(final Object obj) {
351                 if (!(obj instanceof TypedBaseTable)) {
352                     return false;
353                 }
354                 TypedBaseTable<?> typedRowObj = (TypedBaseTable<?>)obj;
355                 return Objects.equal(row, typedRowObj.getRow());
356             }
357
358             @Override
359             public int hashCode() {
360                 if (row == null) {
361                     return 0;
362                 }
363                 return row.hashCode();
364             }
365
366             @Override
367             public String toString() {
368                 String tableName;
369                 TableSchema<?> schema = (TableSchema<?>)processGetTableSchema();
370                 if (schema != null) {
371                     tableName = schema.getName();
372                 } else {
373                     tableName = "";
374                 }
375                 if (row == null) {
376                     return tableName;
377                 }
378                 return tableName + " : " + row.toString();
379             }
380         }
381         );
382     }
383
384     /**
385      * This method extracts all row updates of Class&lt;T&gt; klazz from a TableUpdates
386      * that correspond to insertion or updates of rows of type klazz.
387      * Example:
388      * <code>
389      * Map&lt;UUID,Bridge&gt; updatedBridges = extractRowsUpdated(Bridge.class,updates,dbSchema)
390      * </code>
391      *
392      * @param klazz Class for row type to be extracted
393      * @param updates TableUpdates from which to extract rowUpdates
394      * @param dbSchema Dbschema for the TableUpdates
395      * @return Map&lt;UUID,T&gt; for the type of things being sought
396      */
397     public static <T> Map<UUID,T> extractRowsUpdated(final Class<T> klazz, final TableUpdates updates,
398             final DatabaseSchema dbSchema) {
399         Preconditions.checkNotNull(klazz);
400         Preconditions.checkNotNull(updates);
401         Preconditions.checkNotNull(dbSchema);
402         Map<UUID,T> result = new HashMap<>();
403         Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rowUpdates =
404                 extractRowUpdates(klazz,updates,dbSchema);
405         for (TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema> rowUpdate : rowUpdates.values()) {
406             if (rowUpdate != null && rowUpdate.getNew() != null) {
407                 Row<GenericTableSchema> row = rowUpdate.getNew();
408                 result.put(rowUpdate.getUuid(),TyperUtils.getTypedRowWrapper(dbSchema,klazz,row));
409             }
410         }
411         return result;
412     }
413
414     /**
415      * This method extracts all row updates of Class&lt;T&gt; klazz from a TableUpdates
416      * that correspond to old version of rows of type klazz that have been updated.
417      * Example:
418      * <code>
419      * Map&lt;UUID,Bridge&gt; oldBridges = extractRowsOld(Bridge.class,updates,dbSchema)
420      * </code>
421      *
422      * @param klazz Class for row type to be extracted
423      * @param updates TableUpdates from which to extract rowUpdates
424      * @param dbSchema Dbschema for the TableUpdates
425      * @return Map&lt;UUID,T&gt; for the type of things being sought
426      */
427     public static <T> Map<UUID, T> extractRowsOld(final Class<T> klazz, final TableUpdates updates,
428             final DatabaseSchema dbSchema) {
429         Preconditions.checkNotNull(klazz);
430         Preconditions.checkNotNull(updates);
431         Preconditions.checkNotNull(dbSchema);
432         Map<UUID,T> result = new HashMap<>();
433         Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rowUpdates =
434                 extractRowUpdates(klazz,updates,dbSchema);
435         for (TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema> rowUpdate : rowUpdates.values()) {
436             if (rowUpdate != null && rowUpdate.getOld() != null) {
437                 Row<GenericTableSchema> row = rowUpdate.getOld();
438                 result.put(rowUpdate.getUuid(),TyperUtils.getTypedRowWrapper(dbSchema,klazz,row));
439             }
440         }
441         return result;
442     }
443
444     /**
445      * This method extracts all row updates of Class&lt;T&gt; klazz from a TableUpdates
446      * that correspond to removal of rows of type klazz.
447      * Example:
448      * <code>
449      * Map&lt;UUID,Bridge&gt; updatedBridges = extractRowsRemoved(Bridge.class,updates,dbSchema)
450      * </code>
451      *
452      * @param klazz Class for row type to be extracted
453      * @param updates TableUpdates from which to extract rowUpdates
454      * @param dbSchema Dbschema for the TableUpdates
455      * @return Map&lt;UUID,T&gt; for the type of things being sought
456      */
457     public static <T> Map<UUID,T> extractRowsRemoved(final Class<T> klazz, final TableUpdates updates,
458             final DatabaseSchema dbSchema) {
459         Preconditions.checkNotNull(klazz);
460         Preconditions.checkNotNull(updates);
461         Preconditions.checkNotNull(dbSchema);
462         Map<UUID,T> result = new HashMap<>();
463         Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rowUpdates =
464                 extractRowUpdates(klazz,updates,dbSchema);
465         for (TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema> rowUpdate : rowUpdates.values()) {
466             if (rowUpdate != null && rowUpdate.getNew() == null && rowUpdate.getOld() != null) {
467                 Row<GenericTableSchema> row = rowUpdate.getOld();
468                 result.put(rowUpdate.getUuid(),TyperUtils.getTypedRowWrapper(dbSchema,klazz,row));
469             }
470         }
471         return result;
472     }
473
474     /**
475      * This method extracts all RowUpdates of Class&lt;T&gt; klazz from a TableUpdates
476      * that correspond to rows of type klazz.
477      * Example:
478      * <code>
479      * Map&lt;UUID,TableUpdate&lt;GenericTableSchema&gt;.RowUpdate&lt;GenericTableSchema&gt;&gt; updatedBridges =
480      *     extractRowsUpdates(Bridge.class,updates,dbSchema)
481      * </code>
482      *
483      * @param klazz Class for row type to be extracted
484      * @param updates TableUpdates from which to extract rowUpdates
485      * @param dbSchema Dbschema for the TableUpdates
486      * @return Map&lt;UUID,TableUpdate&lt;GenericTableSchema&gt;.RowUpdate&lt;GenericTableSchema&gt;&gt;
487      *     for the type of things being sought
488      */
489     static Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> extractRowUpdates(
490             final Class<?> klazz,final TableUpdates updates,final DatabaseSchema dbSchema) {
491         Preconditions.checkNotNull(klazz);
492         Preconditions.checkNotNull(updates);
493         Preconditions.checkNotNull(dbSchema);
494         Map<UUID, TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> result =
495                 new HashMap<>();
496         TableUpdate<GenericTableSchema> update = updates.getUpdate(TyperUtils.getTableSchema(dbSchema, klazz));
497         if (update != null) {
498             Map<UUID, TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rows = update.getRows();
499             if (rows != null) {
500                 result = rows;
501             }
502         }
503         return result;
504     }
505 }