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