2 * Copyright © 2019 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.ovsdb.lib.schema.typed;
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.cache.CacheBuilder;
14 import com.google.common.cache.CacheLoader;
15 import com.google.common.cache.LoadingCache;
16 import com.google.common.collect.ImmutableMap;
17 import com.google.common.collect.Maps;
18 import com.google.common.collect.Range;
19 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
20 import java.lang.reflect.Method;
21 import java.util.Locale;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.opendaylight.ovsdb.lib.error.ColumnSchemaNotFoundException;
25 import org.opendaylight.ovsdb.lib.error.SchemaVersionMismatchException;
26 import org.opendaylight.ovsdb.lib.error.TableSchemaNotFoundException;
27 import org.opendaylight.ovsdb.lib.error.TyperException;
28 import org.opendaylight.ovsdb.lib.notation.Row;
29 import org.opendaylight.ovsdb.lib.notation.Version;
30 import org.opendaylight.ovsdb.lib.schema.ColumnSchema;
31 import org.opendaylight.ovsdb.lib.schema.DatabaseSchema;
32 import org.opendaylight.ovsdb.lib.schema.GenericTableSchema;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * Table to Method runtime-constant support. The binding of Class methods to corresponding data operations is defined
38 * by annotations, which means that such mapping is Class-invariant. This invariance is captured in this class.
41 * Data operations are always invoked in the context of a runtime {@link DatabaseSchema}, i.e. for a particular device
42 * or a device function. This class exposes {@link #bindToSchema(TypedDatabaseSchema)}, which will construct an
43 * immutable mapping between a Method and its invocation handler.
45 final class MethodDispatch {
46 abstract static class Invoker {
48 abstract Object invokeMethod(Row<GenericTableSchema> row, Object proxy, Object[] args);
51 abstract static class Prototype {
53 abstract Invoker bindTo(TypedDatabaseSchema dbSchema);
56 abstract static class FailedInvoker extends Invoker {
57 @SuppressFBWarnings(value = "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", justification = "Polymorphic throw")
59 final Object invokeMethod(final Row<GenericTableSchema> row, final Object proxy, final Object[] args) {
63 abstract @NonNull RuntimeException newException();
66 abstract static class TableInvoker extends Invoker {
67 private final GenericTableSchema tableSchema;
69 TableInvoker(final GenericTableSchema tableSchema) {
70 this.tableSchema = tableSchema;
73 @Nullable GenericTableSchema tableSchema() {
78 abstract static class ColumnInvoker<T> extends TableInvoker {
79 private final ColumnSchema<GenericTableSchema, T> columnSchema;
81 ColumnInvoker(final GenericTableSchema tableSchema, final ColumnSchema<GenericTableSchema, T> columnSchema) {
82 super(requireNonNull(tableSchema));
83 this.columnSchema = columnSchema;
87 final @NonNull GenericTableSchema tableSchema() {
88 return verifyNotNull(super.tableSchema());
91 @Nullable ColumnSchema<GenericTableSchema, T> columnSchema() {
96 Object invokeMethod(final Row<GenericTableSchema> row, final Object proxy, final Object[] args) {
97 // When the row is null, that might indicate that the user maybe interested
98 // only in the ColumnSchema and not on the Data.
99 return row == null ? invokeMethod(proxy, args) : invokeRowMethod(row, proxy, args);
102 abstract Object invokeMethod(Object proxy, Object[] args);
104 abstract Object invokeRowMethod(@NonNull Row<GenericTableSchema> row, Object proxy, Object[] args);
107 // As the mode of invocation for a particular method is invariant, we keep the set of dynamically-supported method
108 // in a per-Class cache, thus skipping reflective operations at invocation time.
109 private static final LoadingCache<Class<?>, MethodDispatch> CACHE = CacheBuilder.newBuilder()
110 .weakKeys().weakValues().build(new CacheLoader<Class<?>, MethodDispatch>() {
112 public MethodDispatch load(final Class<?> key) {
113 return new MethodDispatch(key);
117 private abstract static class VersionedPrototype extends Prototype {
118 private static final Logger LOG = LoggerFactory.getLogger(VersionedPrototype.class);
120 private final Range<Version> supportedVersions;
122 VersionedPrototype(final Range<Version> supportedVersions) {
123 this.supportedVersions = requireNonNull(supportedVersions);
127 final Invoker bindTo(final TypedDatabaseSchema dbSchema) {
128 final Version version = dbSchema.getVersion();
129 if (supportedVersions.contains(version)) {
130 return bindToImpl(dbSchema);
133 LOG.debug("Version {} does not match required range {}, deferring failure to invocation time", version,
135 return new FailedInvoker() {
137 RuntimeException newException() {
138 return new SchemaVersionMismatchException(version, supportedVersions);
143 abstract Invoker bindToImpl(TypedDatabaseSchema dbSchema);
146 abstract static class TablePrototype extends VersionedPrototype {
147 private static final Logger LOG = LoggerFactory.getLogger(TablePrototype.class);
149 private final String tableName;
151 TablePrototype(final Range<Version> supportedVersions, final String tableName) {
152 super(supportedVersions);
153 this.tableName = requireNonNull(tableName);
156 final @Nullable GenericTableSchema findTableSchema(final DatabaseSchema dbSchema) {
157 return dbSchema.table(tableName, GenericTableSchema.class);
160 final @NonNull FailedInvoker tableSchemaNotFound(final DatabaseSchema dbSchema) {
161 final String dbName = dbSchema.getName();
162 LOG.debug("Failed to find schema for table {} in {}, deferring failure to invocation time", tableName,
164 return new FailedInvoker() {
166 RuntimeException newException() {
167 return new TableSchemaNotFoundException(tableName, dbName);
173 abstract static class ColumnPrototype<T> extends TablePrototype {
174 private static final Logger LOG = LoggerFactory.getLogger(ColumnPrototype.class);
176 private final @NonNull Class<T> columnType;
177 private final @NonNull String columnName;
179 ColumnPrototype(final Method method, final Class<?> columnType, final String tableName,
180 final String columnName) {
181 super(TypedReflections.getColumnVersionRange(method), tableName);
182 this.columnName = requireNonNull(columnName);
183 this.columnType = requireNonNull((Class<T>) columnType);
186 final @NonNull String columnName() {
190 final @Nullable ColumnSchema<GenericTableSchema, T> findColumnSchema(
191 final @NonNull GenericTableSchema tableSchema) {
192 return tableSchema.column(columnName, columnType);
195 final @NonNull FailedInvoker columnSchemaNotFound(final @NonNull GenericTableSchema tableSchema) {
196 final String tableName = tableSchema.getName();
197 LOG.debug("Failed to find schema for column {} in {}, deferring failure to invocation time", columnName,
199 return new FailedInvoker() {
201 RuntimeException newException() {
202 return new ColumnSchemaNotFoundException(columnName, tableName);
208 final Invoker bindToImpl(final TypedDatabaseSchema dbSchema) {
209 final GenericTableSchema tableSchema = findTableSchema(dbSchema);
210 return tableSchema != null ? bindToImpl(tableSchema) : tableSchemaNotFound(dbSchema);
213 abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema);
216 abstract static class StrictColumnPrototype<T> extends ColumnPrototype<T> {
218 StrictColumnPrototype(final Method method, final String tableName, final String columnName) {
219 super(method, method.getReturnType(), tableName, columnName);
223 final Invoker bindToImpl(@NonNull final GenericTableSchema tableSchema) {
224 final ColumnSchema<GenericTableSchema, T> columnSchema = findColumnSchema(tableSchema);
225 return columnSchema != null ? bindToImpl(tableSchema, columnSchema) : columnSchemaNotFound(tableSchema);
229 abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema,
230 @NonNull ColumnSchema<GenericTableSchema, T> columnSchema);
233 private final @NonNull ImmutableMap<Method, Prototype> prototypes;
234 private final @NonNull String tableName;
236 private MethodDispatch(final Class<?> key) {
237 tableName = TypedReflections.getTableName(key);
239 final ImmutableMap.Builder<Method, Prototype> builder = ImmutableMap.builder();
240 for (Method method : key.getMethods()) {
241 final Prototype prototype = MethodDispatch.prototypeFor(tableName, method);
242 if (prototype != null) {
243 builder.put(method, prototype);
246 prototypes = builder.build();
249 static MethodDispatch forTarget(final Class<?> target) {
250 return CACHE.getUnchecked(target);
253 @NonNull TypedRowInvocationHandler bindToSchema(final TypedDatabaseSchema dbSchema) {
254 return new TypedRowInvocationHandler(tableName,
255 ImmutableMap.copyOf(Maps.transformValues(prototypes, prototype -> prototype.bindTo(dbSchema))));
258 private static @Nullable Prototype prototypeFor(final String tableName, final Method method) {
259 final TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
260 if (typedColumn != null) {
261 final MethodType methodType = typedColumn.method();
262 switch (methodType) {
264 return new GetColumn<>(method, tableName, typedColumn.name());
266 return new GetData<>(method, tableName, typedColumn.name());
268 return GetRow.INSTANCE;
270 return new GetTable(tableName);
272 return new SetData<>(method, tableName, typedColumn.name());
274 throw new TyperException("Unhandled method type " + methodType);
279 * Attempting to get the column name by parsing the method name with a following convention :
280 * 1. GETDATA : get<ColumnName>
281 * 2. SETDATA : set<ColumnName>
282 * 3. GETCOLUMN : get<ColumnName>Column
283 * where <ColumnName> is the name of the column that we are interested in.
285 final String name = method.getName();
286 if (name.startsWith("set")) {
287 return new SetData<>(method, accessorName(name.substring(3)), tableName);
289 if (name.startsWith("get")) {
290 if (name.endsWith("Row")) {
291 return GetRow.INSTANCE;
293 final String tail = name.substring(3);
294 if (tail.endsWith("Column")) {
295 return new GetColumn<>(method, tableName, accessorName(tail.substring(0, tail.length() - 6)));
297 return new GetData<>(method, accessorName(tail), tableName);
303 private static String accessorName(final String columnName) {
304 return columnName.toLowerCase(Locale.ROOT);