/* * 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 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 { @SuppressFBWarnings(value = "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", justification = "Polymorphic throw") @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); } } 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); } }