From: Robert Varga Date: Sun, 1 Dec 2019 13:34:50 +0000 (+0100) Subject: Rework TypedRowInvocationHandler invocation path X-Git-Tag: release/magnesium~59 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=726107ed803ed48519e256e6f274e543ff6def25;p=ovsdb.git Rework TypedRowInvocationHandler invocation path Performing schema lookup/validation is something we usually do only once for a particular schema and then we can freely perform invocation. Introduce MethodDispatch{Invoker,Prototype} constructs. MethodDispatch serves as a the result of reflective analysis of a particular access class. When TypedDatabaseSchema accesses that class, the MethodDispatch is loaded from a global cache and is bound to the schema, resulting in an Invoker map. That map is then used to dispatch InvocationHandler requests to the appropriate logic. Change-Id: Ic97751992fea97b01a6fcfd124edbe783bc86e1c Signed-off-by: Robert Varga --- diff --git a/library/impl/pom.xml b/library/impl/pom.xml index 3294c5b66..7ce2058df 100644 --- a/library/impl/pom.xml +++ b/library/impl/pom.xml @@ -26,6 +26,9 @@ and is available at http://www.eclipse.org/legal/epl-v10.html ODL :: ovsdb :: ${project.artifactId} + + true + ../it/target/jacoco-it.exec diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/error/SchemaVersionMismatchException.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/error/SchemaVersionMismatchException.java index 4d982c49f..55d44a4d3 100644 --- a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/error/SchemaVersionMismatchException.java +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/error/SchemaVersionMismatchException.java @@ -8,6 +8,7 @@ package org.opendaylight.ovsdb.lib.error; +import com.google.common.collect.Range; import org.opendaylight.ovsdb.lib.notation.Version; /** @@ -17,9 +18,8 @@ import org.opendaylight.ovsdb.lib.notation.Version; public class SchemaVersionMismatchException extends RuntimeException { private static final long serialVersionUID = -5194270510726950745L; - public SchemaVersionMismatchException(final Version schemaVersion, final Version fromVersion, - final Version untilVersion) { + public SchemaVersionMismatchException(final Version schemaVersion, final Range range) { super("The schema version used to access the table/column (" + schemaVersion + ") does not match the required" - + " version (from " + fromVersion + " to " + untilVersion + ")"); + + " version range " + range); } } diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetColumn.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetColumn.java new file mode 100644 index 000000000..e92c1794a --- /dev/null +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetColumn.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2019 PANTHEON.tech, s.r.o. 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 java.lang.reflect.Method; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.ovsdb.lib.notation.Column; +import org.opendaylight.ovsdb.lib.notation.Row; +import org.opendaylight.ovsdb.lib.schema.ColumnSchema; +import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; + +final class GetColumn extends MethodDispatch.StrictColumnPrototype { + private static final class Invoker extends MethodDispatch.ColumnInvoker { + Invoker(final @NonNull GenericTableSchema tableSchema, + final @NonNull ColumnSchema columnSchema) { + super(tableSchema, columnSchema); + } + + @Override + Object invokeMethod(final Object proxy, final Object[] args) { + return new Column<>(columnSchema(), null); + } + + @Override + Object invokeRowMethod(final Row row, final Object proxy, final Object[] args) { + return row.getColumn(columnSchema()); + } + } + + GetColumn(final Method method, final String tableName, final String columnName) { + super(method, tableName, columnName); + } + + @Override + Invoker bindToImpl(final GenericTableSchema tableSchema, + final ColumnSchema columnSchema) { + return new Invoker<>(tableSchema, columnSchema); + } +} diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetData.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetData.java new file mode 100644 index 000000000..172cfdb5f --- /dev/null +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetData.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2019 PANTHEON.tech, s.r.o. 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 + */ +/* + * Copyright © 2019 PANTHEON.tech, s.r.o. 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 java.lang.reflect.Method; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.ovsdb.lib.notation.Column; +import org.opendaylight.ovsdb.lib.notation.Row; +import org.opendaylight.ovsdb.lib.schema.ColumnSchema; +import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; + +final class GetData extends MethodDispatch.StrictColumnPrototype { + private static final class Invoker extends MethodDispatch.ColumnInvoker { + Invoker(final @NonNull GenericTableSchema tableSchema, + final @NonNull ColumnSchema columnSchema) { + super(tableSchema, columnSchema); + } + + @Override + Object invokeMethod(final Object proxy, final Object[] args) { + return null; + } + + @Override + Object invokeRowMethod(final Row row, final Object proxy, final Object[] args) { + final Column column = row.getColumn(columnSchema()); + return column == null ? null : column.getData(); + } + } + + GetData(final Method method, final String tableName, final String columnName) { + super(method, tableName, columnName); + } + + @Override + Invoker bindToImpl(final GenericTableSchema tableSchema, + final ColumnSchema columnSchema) { + return new Invoker<>(tableSchema, columnSchema); + } +} diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetRow.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetRow.java new file mode 100644 index 000000000..d93823187 --- /dev/null +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetRow.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2019 PANTHEON.tech, s.r.o. 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 org.opendaylight.ovsdb.lib.notation.Row; +import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; +import org.opendaylight.ovsdb.lib.schema.typed.MethodDispatch.Prototype; + +final class GetRow extends Prototype { + private static final MethodDispatch.Invoker INVOKER = new MethodDispatch.Invoker() { + @Override + Object invokeMethod(final Row row, final Object proxy, final Object[] args) { + return row; + } + }; + + static final GetRow INSTANCE = new GetRow(); + + private GetRow() { + + } + + @Override + MethodDispatch.Invoker bindTo(final TypedDatabaseSchema dbSchema) { + return INVOKER; + } +} diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetTable.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetTable.java new file mode 100644 index 000000000..32c951c00 --- /dev/null +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetTable.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2019 PANTHEON.tech, s.r.o. 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.collect.Range; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.ovsdb.lib.notation.Row; +import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; + +public final class GetTable extends MethodDispatch.TablePrototype { + private static final class Invoker extends MethodDispatch.TableInvoker { + Invoker(final GenericTableSchema tableSchema) { + super(tableSchema); + } + + @Override + @Nullable + Object invokeMethod(final Row row, final Object proxy, final Object[] args) { + return tableSchema(); + } + } + + GetTable(final String tableName) { + super(Range.all(), tableName); + } + + @Override + Invoker bindToImpl(final TypedDatabaseSchema dbSchema) { + return new Invoker(findTableSchema(dbSchema)); + } +} + diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/MethodDispatch.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/MethodDispatch.java new file mode 100644 index 000000000..8620f53e0 --- /dev/null +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/MethodDispatch.java @@ -0,0 +1,304 @@ +/* + * Copyright © 2019 PANTHEON.tech, s.r.o. 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 static com.google.common.base.Verify.verifyNotNull; +import static java.util.Objects.requireNonNull; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Range; +import java.lang.reflect.Method; +import java.util.Locale; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +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.notation.Row; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Table to Method runtime-constant support. The binding of Class methods to corresponding data operations is defined + * by annotations, which means that such mapping is Class-invariant. This invariance is captured in this class. + * + *

+ * Data operations are always invoked in the context of a runtime {@link DatabaseSchema}, i.e. for a particular device + * or a device function. This class exposes {@link #bindToSchema(TypedDatabaseSchema)}, which will construct an + * immutable mapping between a Method and its invocation handler. + */ +final class MethodDispatch { + abstract static class Invoker { + + abstract Object invokeMethod(Row row, Object proxy, Object[] args); + } + + abstract static class Prototype { + + abstract Invoker bindTo(TypedDatabaseSchema dbSchema); + } + + abstract static class FailedInvoker extends Invoker { + @Override + final Object invokeMethod(final Row row, final Object proxy, final Object[] args) { + throw newException(); + } + + abstract @NonNull RuntimeException newException(); + } + + abstract static class TableInvoker extends Invoker { + private final GenericTableSchema tableSchema; + + TableInvoker(final GenericTableSchema tableSchema) { + this.tableSchema = tableSchema; + } + + @Nullable GenericTableSchema tableSchema() { + return tableSchema; + } + } + + abstract static class ColumnInvoker extends TableInvoker { + private final ColumnSchema columnSchema; + + ColumnInvoker(final GenericTableSchema tableSchema, final ColumnSchema columnSchema) { + super(requireNonNull(tableSchema)); + this.columnSchema = columnSchema; + } + + @Override + final @NonNull GenericTableSchema tableSchema() { + return verifyNotNull(super.tableSchema()); + } + + @Nullable ColumnSchema columnSchema() { + return columnSchema; + } + + @Override + Object invokeMethod(final Row row, final Object proxy, final Object[] args) { + // When the row is null, that might indicate that the user maybe interested + // only in the ColumnSchema and not on the Data. + return row == null ? invokeMethod(proxy, args) : invokeRowMethod(row, proxy, args); + } + + abstract Object invokeMethod(Object proxy, Object[] args); + + abstract Object invokeRowMethod(@NonNull Row row, Object proxy, Object[] args); + } + + // As the mode of invocation for a particular method is invariant, we keep the set of dynamically-supported method + // in a per-Class cache, thus skipping reflective operations at invocation time. + private static final LoadingCache, MethodDispatch> CACHE = CacheBuilder.newBuilder() + .weakKeys().weakValues().build(new CacheLoader, MethodDispatch>() { + @Override + public MethodDispatch load(final Class key) { + return new MethodDispatch(key); + } + }); + + private abstract static class VersionedPrototype extends Prototype { + private static final Logger LOG = LoggerFactory.getLogger(VersionedPrototype.class); + + private final Range supportedVersions; + + VersionedPrototype(final Range supportedVersions) { + this.supportedVersions = requireNonNull(supportedVersions); + } + + @Override + final Invoker bindTo(final TypedDatabaseSchema dbSchema) { + final Version version = dbSchema.getVersion(); + if (supportedVersions.contains(version)) { + return bindToImpl(dbSchema); + } + + LOG.debug("Version {} does not match required range {}, deferring failure to invocation time", version, + supportedVersions); + return new FailedInvoker() { + @Override + RuntimeException newException() { + return new SchemaVersionMismatchException(version, supportedVersions); + } + }; + } + + abstract Invoker bindToImpl(TypedDatabaseSchema dbSchema); + } + + abstract static class TablePrototype extends VersionedPrototype { + private static final Logger LOG = LoggerFactory.getLogger(TablePrototype.class); + + private final String tableName; + + TablePrototype(final Range supportedVersions, final String tableName) { + super(supportedVersions); + this.tableName = requireNonNull(tableName); + } + + final @Nullable GenericTableSchema findTableSchema(final DatabaseSchema dbSchema) { + return dbSchema.table(tableName, GenericTableSchema.class); + } + + final @NonNull FailedInvoker tableSchemaNotFound(final DatabaseSchema dbSchema) { + final String dbName = dbSchema.getName(); + LOG.debug("Failed to find schema for table {} in {}, deferring failure to invocation time", tableName, + dbName); + return new FailedInvoker() { + @Override + RuntimeException newException() { + return new TableSchemaNotFoundException(tableName, dbName); + } + }; + } + } + + abstract static class ColumnPrototype extends TablePrototype { + private static final Logger LOG = LoggerFactory.getLogger(ColumnPrototype.class); + + private final @NonNull Class columnType; + private final @NonNull String columnName; + + ColumnPrototype(final Method method, final Class columnType, final String tableName, + final String columnName) { + super(TypedReflections.getColumnVersionRange(method), tableName); + this.columnName = requireNonNull(columnName); + this.columnType = requireNonNull((Class) columnType); + } + + final @NonNull String columnName() { + return columnName; + } + + final @Nullable ColumnSchema findColumnSchema( + final @NonNull GenericTableSchema tableSchema) { + return tableSchema.column(columnName, columnType); + } + + final @NonNull FailedInvoker columnSchemaNotFound(final @NonNull GenericTableSchema tableSchema) { + final String tableName = tableSchema.getName(); + LOG.debug("Failed to find schema for column {} in {}, deferring failure to invocation time", columnName, + tableName); + return new FailedInvoker() { + @Override + RuntimeException newException() { + return new ColumnSchemaNotFoundException(columnName, tableName); + } + }; + } + + @Override + final Invoker bindToImpl(final TypedDatabaseSchema dbSchema) { + final GenericTableSchema tableSchema = findTableSchema(dbSchema); + return tableSchema != null ? bindToImpl(tableSchema) : tableSchemaNotFound(dbSchema); + } + + abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema); + } + + abstract static class StrictColumnPrototype extends ColumnPrototype { + + StrictColumnPrototype(final Method method, final String tableName, final String columnName) { + super(method, method.getReturnType(), tableName, columnName); + } + + @Override + final Invoker bindToImpl(@NonNull final GenericTableSchema tableSchema) { + final ColumnSchema columnSchema = findColumnSchema(tableSchema); + return columnSchema != null ? bindToImpl(tableSchema, columnSchema) : columnSchemaNotFound(tableSchema); + + } + + abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema, + @NonNull ColumnSchema columnSchema); + } + + private final @NonNull ImmutableMap prototypes; + private final @NonNull String tableName; + + private MethodDispatch(final Class key) { + tableName = TypedReflections.getTableName(key); + + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Method method : key.getMethods()) { + final Prototype prototype = MethodDispatch.prototypeFor(tableName, method); + if (prototype != null) { + builder.put(method, prototype); + } + } + this.prototypes = builder.build(); + } + + static MethodDispatch forTarget(final Class target) { + return CACHE.getUnchecked(target); + } + + @NonNull TypedRowInvocationHandler bindToSchema(final TypedDatabaseSchema dbSchema) { + return new TypedRowInvocationHandler(tableName, + ImmutableMap.copyOf(Maps.transformValues(prototypes, prototype -> prototype.bindTo(dbSchema)))); + } + + private static @Nullable Prototype prototypeFor(final String tableName, final Method method) { + final TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); + if (typedColumn != null) { + final MethodType methodType = typedColumn.method(); + switch (methodType) { + case GETCOLUMN: + return new GetColumn<>(method, tableName, typedColumn.name()); + case GETDATA: + return new GetData<>(method, tableName, typedColumn.name()); + case GETROW: + return GetRow.INSTANCE; + case GETTABLESCHEMA: + return new GetTable(tableName); + case SETDATA: + return new SetData<>(method, tableName, typedColumn.name()); + default: + throw new TyperException("Unhandled method type " + methodType); + } + } + + /* + * Attempting to get the column name by parsing the method name with a following convention : + * 1. GETDATA : get + * 2. SETDATA : set + * 3. GETCOLUMN : getColumn + * where is the name of the column that we are interested in. + */ + final String name = method.getName(); + if (name.startsWith("set")) { + return new SetData<>(method, accessorName(name.substring(3)), tableName); + } + if (name.startsWith("get")) { + if (name.endsWith("Row")) { + return GetRow.INSTANCE; + } + final String tail = name.substring(3); + if (tail.endsWith("Column")) { + return new GetColumn<>(method, tableName, accessorName(tail.substring(0, tail.length() - 6))); + } + return new GetData<>(method, accessorName(tail), tableName); + } + + return null; + } + + private static String accessorName(final String columnName) { + return columnName.toLowerCase(Locale.ROOT); + } +} diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/SetData.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/SetData.java new file mode 100644 index 000000000..220808a34 --- /dev/null +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/SetData.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2019 PANTHEON.tech, s.r.o. 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 static java.util.Objects.requireNonNull; + +import java.lang.reflect.Method; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.ovsdb.lib.error.TyperException; +import org.opendaylight.ovsdb.lib.notation.Column; +import org.opendaylight.ovsdb.lib.notation.Row; +import org.opendaylight.ovsdb.lib.schema.ColumnSchema; +import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; + +final class SetData extends MethodDispatch.ColumnPrototype { + private static final class Invoker extends MethodDispatch.ColumnInvoker { + private final @NonNull String columnName; + + Invoker(final @NonNull GenericTableSchema tableSchema, final @NonNull String columnName, + final ColumnSchema columnSchema) { + super(tableSchema, columnSchema); + this.columnName = requireNonNull(columnName); + } + + @Override + Object invokeMethod(final Object proxy, final Object [] args) { + throw new UnsupportedOperationException("No backing row supplied"); + } + + @Override + Object invokeRowMethod(final Row row, final Object proxy, final Object[] args) { + row.addColumn(columnName, new Column<>(columnSchema(), (T) args[0])); + return proxy; + } + } + + SetData(final Method method, final String tableName, final String columnName) { + super(method, findTarget(method), tableName, columnName); + } + + @Override + Invoker bindToImpl(final GenericTableSchema tableSchema) { + return new Invoker<>(tableSchema, columnName(), findColumnSchema(tableSchema)); + } + + private static Class findTarget(final Method method) { + final Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length != 1) { + throw new TyperException("Setter method : " + method.getName() + " requires 1 argument"); + } + return paramTypes[0]; + } +} diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedDatabaseSchemaImpl.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedDatabaseSchemaImpl.java index b482b555b..cdf099dcc 100644 --- a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedDatabaseSchemaImpl.java +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedDatabaseSchemaImpl.java @@ -9,6 +9,9 @@ package org.opendaylight.ovsdb.lib.schema.typed; import static java.util.Objects.requireNonNull; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import com.google.common.reflect.Reflection; import java.util.HashMap; import java.util.Map; @@ -21,6 +24,14 @@ import org.opendaylight.ovsdb.lib.schema.ForwardingDatabaseSchema; import org.opendaylight.ovsdb.lib.schema.GenericTableSchema; final class TypedDatabaseSchemaImpl extends ForwardingDatabaseSchema implements TypedDatabaseSchema { + private final LoadingCache, TypedRowInvocationHandler> handlers = CacheBuilder.newBuilder() + .weakKeys().weakValues().build(new CacheLoader, TypedRowInvocationHandler>() { + @Override + public TypedRowInvocationHandler load(final Class key) { + return MethodDispatch.forTarget(key).bindToSchema(TypedDatabaseSchemaImpl.this); + } + }); + private final DatabaseSchema delegate; TypedDatabaseSchemaImpl(final DatabaseSchema delegate) { @@ -40,7 +51,11 @@ final class TypedDatabaseSchemaImpl extends ForwardingDatabaseSchema implements @Override public GenericTableSchema getTableSchema(final Class klazz) { - return table(TypedReflections.getTableName(klazz), GenericTableSchema.class); + return getTableSchema(TypedReflections.getTableName(klazz)); + } + + private GenericTableSchema getTableSchema(final String tableName) { + return table(tableName, GenericTableSchema.class); } @Override @@ -54,10 +69,12 @@ final class TypedDatabaseSchemaImpl extends ForwardingDatabaseSchema implements } TyperUtils.checkVersion(getVersion(), TypedReflections.getTableVersionRange(klazz)); + TypedRowInvocationHandler handler = handlers.getUnchecked(klazz); if (row != null) { - row.setTableSchema(getTableSchema(klazz)); + row.setTableSchema(getTableSchema(handler.getTableName())); + handler = handler.bindToRow(row); } - return Reflection.newProxy(klazz, new TypedRowInvocationHandler(klazz, this, row)); + return Reflection.newProxy(klazz, handler); } @Override diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedRowInvocationHandler.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedRowInvocationHandler.java index f8b96a6d1..7bc62d1fd 100644 --- a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedRowInvocationHandler.java +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedRowInvocationHandler.java @@ -9,224 +9,77 @@ package org.opendaylight.ovsdb.lib.schema.typed; import static java.util.Objects.requireNonNull; +import com.google.common.collect.ImmutableMap; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.util.Locale; import java.util.Objects; -import org.opendaylight.ovsdb.lib.error.ColumnSchemaNotFoundException; -import org.opendaylight.ovsdb.lib.error.TableSchemaNotFoundException; -import org.opendaylight.ovsdb.lib.error.TyperException; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.ovsdb.lib.error.UnsupportedMethodException; -import org.opendaylight.ovsdb.lib.notation.Column; import org.opendaylight.ovsdb.lib.notation.Row; -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.typed.MethodDispatch.Invoker; +/* + * Theory of operation: we have a set of Invoker, which are indexed by method and point to implementations we should + * be invoking. This mapping is data-invariant, end hence we allow rebiding to a different row (which may not be null). + */ final class TypedRowInvocationHandler implements InvocationHandler { - 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 final Class target; - private final DatabaseSchema dbSchema; - private final Row row; + private final @NonNull ImmutableMap invokers; + private final @NonNull String tableName; + private final @Nullable Row row; - TypedRowInvocationHandler(final Class target, final DatabaseSchema dbSchema, - final Row row) { - this.target = requireNonNull(target); - this.dbSchema = requireNonNull(dbSchema); + private TypedRowInvocationHandler(final @NonNull String tableName, + @NonNull final ImmutableMap invokers, final Row row) { + this.tableName = requireNonNull(tableName); + this.invokers = requireNonNull(invokers); this.row = row; } - private Object processGetData(final Method method) { - String columnName = getColumnName(method); - checkColumnSchemaVersion(dbSchema, method); - if (columnName == null) { - throw new TyperException("Error processing Getter : " + method.getName()); - } - GenericTableSchema tableSchema = TyperUtils.getTableSchema(dbSchema, target); - if (tableSchema == null) { - throw new TableSchemaNotFoundException(TypedReflections.getTableName(target), dbSchema.getName()); - } - ColumnSchema columnSchema = - TyperUtils.getColumnSchema(tableSchema, columnName, (Class) method.getReturnType()); - if (columnSchema == null) { - throw new ColumnSchemaNotFoundException(columnName, tableSchema.getName()); - } - if (row == null || row.getColumn(columnSchema) == null) { - return null; - } - return row.getColumn(columnSchema).getData(); - } - - private Object processGetRow() { - return row; - } - - private Object processGetColumn(final Method method) { - String columnName = getColumnName(method); - checkColumnSchemaVersion(dbSchema, method); - if (columnName == null) { - throw new TyperException("Error processing GetColumn : " + method.getName()); - } - GenericTableSchema tableSchema = TyperUtils.getTableSchema(dbSchema, target); - if (tableSchema == null) { - throw new TableSchemaNotFoundException(TypedReflections.getTableName(target), dbSchema.getName()); - } - ColumnSchema columnSchema = - TyperUtils.getColumnSchema(tableSchema, columnName, (Class) method.getReturnType()); - if (columnSchema == null) { - throw new ColumnSchemaNotFoundException(columnName, tableSchema.getName()); - } - // 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(final Object proxy, final Method method, final 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 = TyperUtils.getTableSchema(dbSchema, target); - ColumnSchema columnSchema = - TyperUtils.getColumnSchema(tableSchema, columnName, (Class) args[0].getClass()); - Column column = - new Column<>(columnSchema, args[0]); - row.addColumn(columnName, column); - return proxy; - } - - private GenericTableSchema processGetTableSchema() { - return TyperUtils.getTableSchema(dbSchema, target); - } - - private static Boolean isHashCodeMethod(final Method method, final Object[] args) { - return (args == null || args.length == 0) && method.getName().equals("hashCode"); + TypedRowInvocationHandler(final @NonNull String tableName, @NonNull final ImmutableMap invokers) { + this(tableName, invokers, null); } - private static Boolean isEqualsMethod(final Method method, final Object[] args) { - return args != null - && args.length == 1 - && method.getName().equals("equals") - && Object.class.equals(method.getParameterTypes()[0]); + TypedRowInvocationHandler bindToRow(final @Nullable Row newRow) { + return row == newRow ? this : new TypedRowInvocationHandler(tableName, invokers, newRow); } - private static Boolean isToStringMethod(final Method method, final Object[] args) { - return (args == null || args.length == 0) && method.getName().equals("toString"); + String getTableName() { + return tableName; } @Override - public Object invoke(final Object proxy, final Method method, final 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 processHashCode(); - } else if (isEqualsMethod(method, args)) { - return proxy.getClass().isInstance(args[0]) && processEquals(args[0]); - } else if (isToStringMethod(method, args)) { - return processToString(); + public Object invoke(final Object proxy, final Method method, final Object[] args) { + final Invoker invoker = invokers.get(method); + return invoker != null ? invoker.invokeMethod(row, proxy, args) : invokeObjectMethod(proxy, method, args); + } + + // Split out to aid inlining + private Object invokeObjectMethod(final Object proxy, final Method method, final Object[] args) { + switch (method.getName()) { + case "hashCode": + if (args == null || args.length == 0) { + return row == null ? 0 : row.hashCode(); + } + break; + case "equals": + if (args != null && args.length == 1 && method.getParameterTypes()[0] == Object.class) { + // We only run equality or our proxy and only when it is proxying a TypedBaseTable + final Object obj = args[0]; + return proxy == obj || proxy.getClass().isInstance(obj) && obj instanceof TypedBaseTable + && Objects.equals(row, ((TypedBaseTable)obj).getRow()); + } + break; + case "toString": + if (args == null || args.length == 0) { + return row == null ? tableName : tableName + " : " + row.toString(); + } + break; + default: + break; } - throw new UnsupportedMethodException("Method not supported " + method.toString()); - } - - private boolean processEquals(final Object obj) { - return obj instanceof TypedBaseTable && Objects.equals(row, ((TypedBaseTable)obj).getRow()); - } - - private int processHashCode() { - return row == null ? 0 : row.hashCode(); - } - private String processToString() { - final GenericTableSchema schema = processGetTableSchema(); - final String tableName = schema != null ? schema.getName() : ""; - return row == null ? tableName : tableName + " : " + row.toString(); - } - - private static boolean isGetColumn(final 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(final 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 isGetRow(final 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 isGetTableSchema(final Method method) { - TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); - return typedColumn != null && typedColumn.method().equals(MethodType.GETTABLESCHEMA); - } - - private static boolean isSetData(final Method method) { - TypedColumn typedColumn = method.getAnnotation(TypedColumn.class); - if (typedColumn != null) { - return typedColumn.method().equals(MethodType.SETDATA); - } - - return method.getName().startsWith(SET_STARTS_WITH); - } - - private static void checkColumnSchemaVersion(final DatabaseSchema dbSchema, final Method method) { - TyperUtils.checkVersion(dbSchema.getVersion(), TypedReflections.getColumnVersionRange(method)); - } - - private static String getColumnName(final 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 - * 2. SETDATA : set - * 3. GETCOLUMN : getColumn - * where 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(Locale.ROOT); - } else if (isGetColumn(method)) { - return method.getName().substring(index, method.getName().indexOf(GETCOLUMN_ENDS_WITH, - index)).toLowerCase(Locale.ROOT); - } - - return null; + throw new UnsupportedMethodException("Method not supported " + method.toString()); } } diff --git a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TyperUtils.java b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TyperUtils.java index 61034a8c7..44f610133 100644 --- a/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TyperUtils.java +++ b/library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TyperUtils.java @@ -17,7 +17,6 @@ import org.opendaylight.ovsdb.lib.message.TableUpdates; 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; @@ -50,16 +49,9 @@ public final class TyperUtils { return getTyped(dbSchema).getTableSchema(klazz); } - static ColumnSchema getColumnSchema(final GenericTableSchema tableSchema, - final String columnName, final Class metaClass) { - return tableSchema.column(columnName, metaClass); - } - static void checkVersion(final Version schemaVersion, final Range range) { if (!range.contains(schemaVersion)) { - throw new SchemaVersionMismatchException(schemaVersion, - range.hasLowerBound() ? range.lowerEndpoint() : Version.NULL, - range.hasUpperBound() ? range.upperEndpoint() : Version.NULL); + throw new SchemaVersionMismatchException(schemaVersion, range); } }