Rework TypedRowInvocationHandler invocation path 84/86084/24
authorRobert Varga <robert.varga@pantheon.tech>
Sun, 1 Dec 2019 13:34:50 +0000 (14:34 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 2 Dec 2019 13:03:15 +0000 (14:03 +0100)
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 <robert.varga@pantheon.tech>
library/impl/pom.xml
library/impl/src/main/java/org/opendaylight/ovsdb/lib/error/SchemaVersionMismatchException.java
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetColumn.java [new file with mode: 0644]
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetData.java [new file with mode: 0644]
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetRow.java [new file with mode: 0644]
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/GetTable.java [new file with mode: 0644]
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/MethodDispatch.java [new file with mode: 0644]
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/SetData.java [new file with mode: 0644]
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedDatabaseSchemaImpl.java
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TypedRowInvocationHandler.java
library/impl/src/main/java/org/opendaylight/ovsdb/lib/schema/typed/TyperUtils.java

index 3294c5b66316e3e8bc6717ab209110e6f39bb7a0..7ce2058dfcce7246bc2353d50ffe085a22aa678a 100644 (file)
@@ -26,6 +26,9 @@ and is available at http://www.eclipse.org/legal/epl-v10.html
   <name>ODL :: ovsdb :: ${project.artifactId}</name>
 
   <properties>
+    <!-- Fails build for copy/pasted code -->
+    <pmd.cpd.fail>true</pmd.cpd.fail>
+
     <sonar.jacoco.itReportPath>../it/target/jacoco-it.exec</sonar.jacoco.itReportPath>
   </properties>
 
index 4d982c49f9692d8a6cf787f75e34ad405326fc28..55d44a4d3dfc2b2cb7857b2279e9333ed5e274e1 100644 (file)
@@ -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<Version> 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 (file)
index 0000000..e92c179
--- /dev/null
@@ -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<T> extends MethodDispatch.StrictColumnPrototype<T> {
+    private static final class Invoker<T> extends MethodDispatch.ColumnInvoker<T> {
+        Invoker(final @NonNull GenericTableSchema tableSchema,
+                final @NonNull ColumnSchema<GenericTableSchema, T> columnSchema) {
+            super(tableSchema, columnSchema);
+        }
+
+        @Override
+        Object invokeMethod(final Object proxy, final Object[] args) {
+            return new Column<>(columnSchema(), null);
+        }
+
+        @Override
+        Object invokeRowMethod(final Row<GenericTableSchema> 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<T> bindToImpl(final GenericTableSchema tableSchema,
+            final ColumnSchema<GenericTableSchema, T> 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 (file)
index 0000000..172cfdb
--- /dev/null
@@ -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<T> extends MethodDispatch.StrictColumnPrototype<T> {
+    private static final class Invoker<T> extends MethodDispatch.ColumnInvoker<T> {
+        Invoker(final @NonNull GenericTableSchema tableSchema,
+                final @NonNull ColumnSchema<GenericTableSchema, T> columnSchema) {
+            super(tableSchema, columnSchema);
+        }
+
+        @Override
+        Object invokeMethod(final Object proxy, final Object[] args) {
+            return null;
+        }
+
+        @Override
+        Object invokeRowMethod(final Row<GenericTableSchema> row, final Object proxy, final Object[] args) {
+            final Column<GenericTableSchema, T> 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<T> bindToImpl(final GenericTableSchema tableSchema,
+            final ColumnSchema<GenericTableSchema, T> 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 (file)
index 0000000..d938231
--- /dev/null
@@ -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<GenericTableSchema> 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 (file)
index 0000000..32c951c
--- /dev/null
@@ -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<GenericTableSchema> 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 (file)
index 0000000..8620f53
--- /dev/null
@@ -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.
+ *
+ * <p>
+ * 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<GenericTableSchema> 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<GenericTableSchema> 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<T> extends TableInvoker {
+        private final ColumnSchema<GenericTableSchema, T> columnSchema;
+
+        ColumnInvoker(final GenericTableSchema tableSchema, final ColumnSchema<GenericTableSchema, T> columnSchema) {
+            super(requireNonNull(tableSchema));
+            this.columnSchema = columnSchema;
+        }
+
+        @Override
+        final @NonNull GenericTableSchema tableSchema() {
+            return verifyNotNull(super.tableSchema());
+        }
+
+        @Nullable ColumnSchema<GenericTableSchema, T> columnSchema() {
+            return columnSchema;
+        }
+
+        @Override
+        Object invokeMethod(final Row<GenericTableSchema> 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<GenericTableSchema> 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<Class<?>, MethodDispatch> CACHE = CacheBuilder.newBuilder()
+            .weakKeys().weakValues().build(new CacheLoader<Class<?>, 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<Version> supportedVersions;
+
+        VersionedPrototype(final Range<Version> 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<Version> 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<T> extends TablePrototype {
+        private static final Logger LOG = LoggerFactory.getLogger(ColumnPrototype.class);
+
+        private final @NonNull Class<T> 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<T>) columnType);
+        }
+
+        final @NonNull String columnName() {
+            return columnName;
+        }
+
+        final @Nullable ColumnSchema<GenericTableSchema, T> 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<T> extends ColumnPrototype<T> {
+
+        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<GenericTableSchema, T> columnSchema = findColumnSchema(tableSchema);
+            return columnSchema != null ? bindToImpl(tableSchema, columnSchema) : columnSchemaNotFound(tableSchema);
+
+        }
+
+        abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema,
+                @NonNull ColumnSchema<GenericTableSchema, T> columnSchema);
+    }
+
+    private final @NonNull ImmutableMap<Method, Prototype> prototypes;
+    private final @NonNull String tableName;
+
+    private MethodDispatch(final Class<?> key) {
+        tableName = TypedReflections.getTableName(key);
+
+        final ImmutableMap.Builder<Method, Prototype> 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<ColumnName>
+         * 2. SETDATA : set<ColumnName>
+         * 3. GETCOLUMN : get<ColumnName>Column
+         * where <ColumnName> 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 (file)
index 0000000..220808a
--- /dev/null
@@ -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<T> extends MethodDispatch.ColumnPrototype<T> {
+    private static final class Invoker<T> extends MethodDispatch.ColumnInvoker<T> {
+        private final @NonNull String columnName;
+
+        Invoker(final @NonNull GenericTableSchema tableSchema, final @NonNull String columnName,
+                final ColumnSchema<GenericTableSchema, T> 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<GenericTableSchema> 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<T> 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];
+    }
+}
index b482b555b0962b3df4041d422e04886235e83830..cdf099dcc1341c548f8256ba87f92e80041cd689 100644 (file)
@@ -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<Class<?>, TypedRowInvocationHandler> handlers = CacheBuilder.newBuilder()
+            .weakKeys().weakValues().build(new CacheLoader<Class<?>, 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
index f8b96a6d10db4a131ab8d9cc2a08b57f196d093f..7bc62d1fdcd24b1405dc6f38f1bef1ee9de0bef8 100644 (file)
@@ -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<GenericTableSchema> row;
+    private final @NonNull ImmutableMap<Method, Invoker> invokers;
+    private final @NonNull String tableName;
+    private final @Nullable Row<GenericTableSchema> row;
 
-    TypedRowInvocationHandler(final Class<?> target, final DatabaseSchema dbSchema,
-            final Row<GenericTableSchema> row) {
-        this.target = requireNonNull(target);
-        this.dbSchema = requireNonNull(dbSchema);
+    private TypedRowInvocationHandler(final @NonNull String tableName,
+            @NonNull final ImmutableMap<Method, Invoker> invokers, final Row<GenericTableSchema> 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<GenericTableSchema, Object> columnSchema =
-                TyperUtils.getColumnSchema(tableSchema, columnName, (Class<Object>) 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<GenericTableSchema, Object> columnSchema =
-                TyperUtils.getColumnSchema(tableSchema, columnName, (Class<Object>) 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<GenericTableSchema, Object> columnSchema =
-                TyperUtils.getColumnSchema(tableSchema, columnName, (Class<Object>) args[0].getClass());
-        Column<GenericTableSchema, Object> 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<Method, Invoker> 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<GenericTableSchema> 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<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(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());
     }
 }
index 61034a8c7abd2e104faf6dc182642f58804bdd6d..44f610133f29728b174dfab420829ce232c5c6ec 100644 (file)
@@ -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<GenericTableSchema, Object> getColumnSchema(final GenericTableSchema tableSchema,
-            final String columnName, final Class<Object> metaClass) {
-        return tableSchema.column(columnName, metaClass);
-    }
-
     static void checkVersion(final Version schemaVersion, final Range<Version> 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);
         }
     }