/* * Copyright © 2014, 2017 Red Hat, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.ovsdb.lib.schema.typed; import com.google.common.base.Preconditions; import com.google.common.reflect.Reflection; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import org.opendaylight.ovsdb.lib.error.ColumnSchemaNotFoundException; import org.opendaylight.ovsdb.lib.error.SchemaVersionMismatchException; import org.opendaylight.ovsdb.lib.error.TableSchemaNotFoundException; import org.opendaylight.ovsdb.lib.error.TyperException; import org.opendaylight.ovsdb.lib.error.UnsupportedMethodException; import org.opendaylight.ovsdb.lib.message.TableUpdate; import org.opendaylight.ovsdb.lib.message.TableUpdates; import org.opendaylight.ovsdb.lib.notation.Column; import org.opendaylight.ovsdb.lib.notation.Row; import org.opendaylight.ovsdb.lib.notation.UUID; import org.opendaylight.ovsdb.lib.notation.Version; import org.opendaylight.ovsdb.lib.schema.ColumnSchema; import org.opendaylight.ovsdb.lib.schema.DatabaseSchema; import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; import org.opendaylight.ovsdb.lib.schema.TableSchema; /** * Utility methods for typed OVSDB schema data. */ public class TyperUtils { private static final String GET_STARTS_WITH = "get"; private static final String SET_STARTS_WITH = "set"; private static final String GETCOLUMN_ENDS_WITH = "Column"; private static final String GETROW_ENDS_WITH = "Row"; private TyperUtils() { // Prevent instantiating a utility class } private static <T> String getTableName(Class<T> klazz) { TypedTable typedTable = klazz.getAnnotation(TypedTable.class); if (typedTable != null) { return typedTable.name(); } return klazz.getSimpleName(); } /** * Retrieve the table schema for the given table in the given database schema. * * @param dbSchema The database schema. * @param klazz The class whose table schema should be retrieved. Classes are matched in the database schema either * using their {@link TypedTable} annotation, if they have one, or by name. * @return the table schema. */ public static GenericTableSchema getTableSchema(DatabaseSchema dbSchema, Class<?> klazz) { String tableName = getTableName(klazz); return dbSchema.table(tableName, GenericTableSchema.class); } public static ColumnSchema<GenericTableSchema, Object> getColumnSchema(GenericTableSchema tableSchema, String columnName, Class<Object> metaClass) { return tableSchema.column(columnName, metaClass); } private static String getColumnName(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); if (typedColumn != null) { return typedColumn.name(); } /* * Attempting to get the column name by parsing the method name with a following convention : * 1. GETDATA : get<ColumnName> * 2. SETDATA : set<ColumnName> * 3. GETCOLUMN : get<ColumnName>Column * where <ColumnName> is the name of the column that we are interested in. */ int index = GET_STARTS_WITH.length(); if (isGetData(method) || isSetData(method)) { return method.getName().substring(index, method.getName().length()).toLowerCase(); } else if (isGetColumn(method)) { return method.getName().substring(index, method.getName().indexOf(GETCOLUMN_ENDS_WITH, index)).toLowerCase(); } return null; } private static boolean isGetTableSchema(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); return typedColumn != null && typedColumn.method().equals(MethodType.GETTABLESCHEMA); } private static boolean isGetRow(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); if (typedColumn != null) { return typedColumn.method().equals(MethodType.GETROW); } return method.getName().startsWith(GET_STARTS_WITH) && method.getName().endsWith(GETROW_ENDS_WITH); } private static boolean isGetColumn(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); if (typedColumn != null) { return typedColumn.method().equals(MethodType.GETCOLUMN); } return method.getName().startsWith(GET_STARTS_WITH) && method.getName().endsWith(GETCOLUMN_ENDS_WITH); } private static boolean isGetData(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); if (typedColumn != null) { return typedColumn.method().equals(MethodType.GETDATA); } return method.getName().startsWith(GET_STARTS_WITH) && !method.getName().endsWith(GETCOLUMN_ENDS_WITH); } private static boolean isSetData(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); if (typedColumn != null) { return typedColumn.method().equals(MethodType.SETDATA); } return method.getName().startsWith(SET_STARTS_WITH); } public static Version getColumnFromVersion(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); if (typedColumn != null) { return Version.fromString(typedColumn.fromVersion()); } return Version.NULL; } public static <T> Version getTableFromVersion(final Class<T> klazz) { TypedTable typedTable = klazz.getAnnotation(TypedTable.class); if (typedTable != null) { return Version.fromString(typedTable.fromVersion()); } return Version.NULL; } public static Version getColumnUntilVersion(Method method) { TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); if (typedColumn != null) { return Version.fromString(typedColumn.untilVersion()); } return Version.NULL; } public static <T> Version getTableUntilVersion(final Class<T> klazz) { TypedTable typedTable = klazz.getAnnotation(TypedTable.class); if (typedTable != null) { return Version.fromString(typedTable.untilVersion()); } return Version.NULL; } /** * Method that checks validity of the parameter passed to getTypedRowWrapper. * This method checks for a valid Database Schema matching the expected Database for a given table * and checks for the presence of the Table in Database Schema. * * @param dbSchema DatabaseSchema as learnt from a OVSDB connection * @param klazz Typed Class that represents a Table * @return true if valid, false otherwise */ private static <T> boolean isValid(DatabaseSchema dbSchema, final Class<T> klazz) { if (dbSchema == null) { return false; } TypedTable typedTable = klazz.getAnnotation(TypedTable.class); if (typedTable != null && !dbSchema.getName().equalsIgnoreCase(typedTable.database())) { return false; } checkTableSchemaVersion(dbSchema, klazz); return true; } private static void checkColumnSchemaVersion(DatabaseSchema dbSchema, Method method) { Version fromVersion = getColumnFromVersion(method); Version untilVersion = getColumnUntilVersion(method); Version schemaVersion = dbSchema.getVersion(); checkVersion(schemaVersion, fromVersion, untilVersion); } private static <T> void checkTableSchemaVersion(DatabaseSchema dbSchema, Class<T> klazz) { Version fromVersion = getTableFromVersion(klazz); Version untilVersion = getTableUntilVersion(klazz); Version schemaVersion = dbSchema.getVersion(); checkVersion(schemaVersion, fromVersion, untilVersion); } private static void checkVersion(Version schemaVersion, Version fromVersion, Version untilVersion) { if ((!fromVersion.equals(Version.NULL) && schemaVersion.compareTo(fromVersion) < 0) || (!untilVersion.equals( Version.NULL) && schemaVersion.compareTo(untilVersion) > 0)) { throw new SchemaVersionMismatchException(schemaVersion, fromVersion, untilVersion); } } /** * Returns a Typed Proxy implementation for the klazz passed as a parameter. * Per design choice, the Typed Proxy implementation is just a Wrapper on top of the actual * Row which is untyped. * * <p>Being just a wrapper, it is state-less and more of a convenience functionality to * provide a type-safe infrastructure for the applications to built on top of. * And this Typed infra is completely optional. * * <p>It is the applications responsibility to pass on the raw Row parameter and this method will * return the appropriate Proxy wrapper for the passed klazz Type. * The raw Row parameter may be null if the caller is interested in just the ColumnSchema. * But that is not a very common use-case. * * @param dbSchema DatabaseSchema as learnt from a OVSDB connection * @param klazz Typed Class that represents a Table */ public static <T> T getTypedRowWrapper(final DatabaseSchema dbSchema, final Class<T> klazz) { return getTypedRowWrapper(dbSchema, klazz, new Row<>()); } /** * Returns a Typed Proxy implementation for the klazz passed as a parameter. * Per design choice, the Typed Proxy implementation is just a Wrapper on top of the actual * Row which is untyped. * * <p>Being just a wrapper, it is state-less and more of a convenience functionality * to provide a type-safe infrastructure for the applications to built on top of. * And this Typed infra is completely optional. * * <p>It is the applications responsibility to pass on the raw Row parameter and this method * will return the appropriate Proxy wrapper for the passed klazz Type. * The raw Row parameter may be null if the caller is interested in just the * ColumnSchema. But that is not a very common use-case. * * @param dbSchema DatabaseSchema as learnt from a OVSDB connection * @param klazz Typed Class that represents a Table * @param row The actual Row that the wrapper is operating on. It can be null if the caller * is just interested in getting ColumnSchema. */ public static <T> T getTypedRowWrapper(final DatabaseSchema dbSchema, final Class<T> klazz, final Row<GenericTableSchema> row) { if (!isValid(dbSchema, klazz)) { return null; } if (row != null) { row.setTableSchema(getTableSchema(dbSchema, klazz)); } return Reflection.newProxy(klazz, new InvocationHandler() { private Object processGetData(Method method) { String columnName = getColumnName(method); checkColumnSchemaVersion(dbSchema, method); if (columnName == null) { throw new TyperException("Error processing Getter : " + method.getName()); } GenericTableSchema tableSchema = getTableSchema(dbSchema, klazz); if (tableSchema == null) { String message = TableSchemaNotFoundException.createMessage(getTableName(klazz), dbSchema.getName()); throw new TableSchemaNotFoundException(message); } ColumnSchema<GenericTableSchema, Object> columnSchema = getColumnSchema(tableSchema, columnName, (Class<Object>) method.getReturnType()); if (columnSchema == null) { String message = ColumnSchemaNotFoundException.createMessage(columnName, tableSchema.getName()); throw new ColumnSchemaNotFoundException(message); } if (row == null || row.getColumn(columnSchema) == null) { return null; } return row.getColumn(columnSchema).getData(); } private Object processGetRow() { return row; } private Object processGetColumn(Method method) { String columnName = getColumnName(method); checkColumnSchemaVersion(dbSchema, method); if (columnName == null) { throw new TyperException("Error processing GetColumn : " + method.getName()); } GenericTableSchema tableSchema = getTableSchema(dbSchema, klazz); if (tableSchema == null) { String message = TableSchemaNotFoundException.createMessage(getTableName(klazz), dbSchema.getName()); throw new TableSchemaNotFoundException(message); } ColumnSchema<GenericTableSchema, Object> columnSchema = getColumnSchema(tableSchema, columnName, (Class<Object>) method.getReturnType()); if (columnSchema == null) { String message = ColumnSchemaNotFoundException.createMessage(columnName, tableSchema.getName()); throw new ColumnSchemaNotFoundException(message); } // When the row is null, that might indicate that the user maybe interested // only in the ColumnSchema and not on the Data. if (row == null) { return new Column<>(columnSchema, null); } return row.getColumn(columnSchema); } private Object processSetData(Object proxy, Method method, Object[] args) { if (args == null || args.length != 1) { throw new TyperException("Setter method : " + method.getName() + " requires 1 argument"); } checkColumnSchemaVersion(dbSchema, method); String columnName = getColumnName(method); if (columnName == null) { throw new TyperException("Unable to locate Column Name for " + method.getName()); } GenericTableSchema tableSchema = getTableSchema(dbSchema, klazz); ColumnSchema<GenericTableSchema, Object> columnSchema = getColumnSchema(tableSchema, columnName, (Class<Object>) args[0].getClass()); Column<GenericTableSchema, Object> column = new Column<>(columnSchema, args[0]); row.addColumn(columnName, column); return proxy; } private Object processGetTableSchema() { if (dbSchema == null) { return null; } return getTableSchema(dbSchema, klazz); } private Boolean isHashCodeMethod(Method method, Object[] args) { return (args == null || args.length == 0) && method.getName().equals("hashCode"); } private Boolean isEqualsMethod(Method method, Object[] args) { return (args != null && args.length == 1 && method.getName().equals("equals") && Object.class.equals(method.getParameterTypes()[0])); } private Boolean isToStringMethod(Method method, Object[] args) { return (args == null || args.length == 0) && method.getName().equals("toString"); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Exception { if (isGetTableSchema(method)) { return processGetTableSchema(); } else if (isGetRow(method)) { return processGetRow(); } else if (isSetData(method)) { return processSetData(proxy, method, args); } else if (isGetData(method)) { return processGetData(method); } else if (isGetColumn(method)) { return processGetColumn(method); } else if (isHashCodeMethod(method, args)) { return hashCode(); } else if (isEqualsMethod(method, args)) { return proxy.getClass().isInstance(args[0]) && this.equals(args[0]); } else if (isToStringMethod(method, args)) { return this.toString(); } throw new UnsupportedMethodException("Method not supported " + method.toString()); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } TypedBaseTable<?> typedRowObj = (TypedBaseTable<?>)obj; if (row == null && typedRowObj.getRow() == null) { return true; } if (row.equals(typedRowObj.getRow())) { return true; } return false; } @Override public int hashCode() { if (row == null) { return 0; } return row.hashCode(); } @Override public String toString() { String tableName; TableSchema<?> schema = (TableSchema<?>)processGetTableSchema(); if (schema != null) { tableName = schema.getName(); } else { tableName = ""; } if (row == null) { return tableName; } return tableName + " : " + row.toString(); } } ); } /** * This method extracts all row updates of Class<T> klazz from a TableUpdates * that correspond to insertion or updates of rows of type klazz. * Example: * <code> * Map<UUID,Bridge> updatedBridges = extractRowsUpdated(Bridge.class,updates,dbSchema) * </code> * * @param klazz Class for row type to be extracted * @param updates TableUpdates from which to extract rowUpdates * @param dbSchema Dbschema for the TableUpdates * @return Map<UUID,T> for the type of things being sought */ public static <T> Map<UUID,T> extractRowsUpdated(Class<T> klazz,TableUpdates updates,DatabaseSchema dbSchema) { Preconditions.checkNotNull(klazz); Preconditions.checkNotNull(updates); Preconditions.checkNotNull(dbSchema); Map<UUID,T> result = new HashMap<>(); Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rowUpdates = extractRowUpdates(klazz,updates,dbSchema); for (TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema> rowUpdate : rowUpdates.values()) { if (rowUpdate != null && rowUpdate.getNew() != null) { Row<GenericTableSchema> row = rowUpdate.getNew(); result.put(rowUpdate.getUuid(),TyperUtils.getTypedRowWrapper(dbSchema,klazz,row)); } } return result; } /** * This method extracts all row updates of Class<T> klazz from a TableUpdates * that correspond to old version of rows of type klazz that have been updated. * Example: * <code> * Map<UUID,Bridge> oldBridges = extractRowsOld(Bridge.class,updates,dbSchema) * </code> * * @param klazz Class for row type to be extracted * @param updates TableUpdates from which to extract rowUpdates * @param dbSchema Dbschema for the TableUpdates * @return Map<UUID,T> for the type of things being sought */ public static <T> Map<UUID, T> extractRowsOld(Class<T> klazz, TableUpdates updates, DatabaseSchema dbSchema) { Preconditions.checkNotNull(klazz); Preconditions.checkNotNull(updates); Preconditions.checkNotNull(dbSchema); Map<UUID,T> result = new HashMap<>(); Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rowUpdates = extractRowUpdates(klazz,updates,dbSchema); for (TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema> rowUpdate : rowUpdates.values()) { if (rowUpdate != null && rowUpdate.getOld() != null) { Row<GenericTableSchema> row = rowUpdate.getOld(); result.put(rowUpdate.getUuid(),TyperUtils.getTypedRowWrapper(dbSchema,klazz,row)); } } return result; } /** * This method extracts all row updates of Class<T> klazz from a TableUpdates * that correspond to removal of rows of type klazz. * Example: * <code> * Map<UUID,Bridge> updatedBridges = extractRowsRemoved(Bridge.class,updates,dbSchema) * </code> * * @param klazz Class for row type to be extracted * @param updates TableUpdates from which to extract rowUpdates * @param dbSchema Dbschema for the TableUpdates * @return Map<UUID,T> for the type of things being sought */ public static <T> Map<UUID,T> extractRowsRemoved(Class<T> klazz,TableUpdates updates,DatabaseSchema dbSchema) { Preconditions.checkNotNull(klazz); Preconditions.checkNotNull(updates); Preconditions.checkNotNull(dbSchema); Map<UUID,T> result = new HashMap<>(); Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rowUpdates = extractRowUpdates(klazz,updates,dbSchema); for (TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema> rowUpdate : rowUpdates.values()) { if (rowUpdate != null && rowUpdate.getNew() == null && rowUpdate.getOld() != null) { Row<GenericTableSchema> row = rowUpdate.getOld(); result.put(rowUpdate.getUuid(),TyperUtils.getTypedRowWrapper(dbSchema,klazz,row)); } } return result; } /** * This method extracts all RowUpdates of Class<T> klazz from a TableUpdates * that correspond to rows of type klazz. * Example: * <code> * Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> updatedBridges = * extractRowsUpdates(Bridge.class,updates,dbSchema) * </code> * * @param klazz Class for row type to be extracted * @param updates TableUpdates from which to extract rowUpdates * @param dbSchema Dbschema for the TableUpdates * @return Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> * for the type of things being sought */ public static Map<UUID,TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> extractRowUpdates(Class<?> klazz,TableUpdates updates,DatabaseSchema dbSchema) { Preconditions.checkNotNull(klazz); Preconditions.checkNotNull(updates); Preconditions.checkNotNull(dbSchema); Map<UUID, TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> result = new HashMap<>(); TableUpdate<GenericTableSchema> update = updates.getUpdate(TyperUtils.getTableSchema(dbSchema, klazz)); if (update != null) { Map<UUID, TableUpdate<GenericTableSchema>.RowUpdate<GenericTableSchema>> rows = update.getRows(); if (rows != null) { result = rows; } } return result; } }