Bump upstreams for 2022.09 Chlorine
[ovsdb.git] / library / impl / src / main / java / org / opendaylight / ovsdb / lib / schema / typed / MethodDispatch.java
1 /*
2  * Copyright © 2019 PANTHEON.tech, s.r.o. and others. All rights reserved.
3  *
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
7  */
8 package org.opendaylight.ovsdb.lib.schema.typed;
9
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
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;
35
36 /**
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.
39  *
40  * <p>
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.
44  */
45 final class MethodDispatch {
46     abstract static class Invoker {
47
48         abstract Object invokeMethod(Row<GenericTableSchema> row, Object proxy, Object[] args);
49     }
50
51     abstract static class Prototype {
52
53         abstract Invoker bindTo(TypedDatabaseSchema dbSchema);
54     }
55
56     abstract static class FailedInvoker extends Invoker {
57         @SuppressFBWarnings(value = "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", justification = "Polymorphic throw")
58         @Override
59         final Object invokeMethod(final Row<GenericTableSchema> row, final Object proxy, final Object[] args) {
60             throw newException();
61         }
62
63         abstract @NonNull RuntimeException newException();
64     }
65
66     abstract static class TableInvoker extends Invoker {
67         private final GenericTableSchema tableSchema;
68
69         TableInvoker(final GenericTableSchema tableSchema) {
70             this.tableSchema = tableSchema;
71         }
72
73         @Nullable GenericTableSchema tableSchema() {
74             return tableSchema;
75         }
76     }
77
78     abstract static class ColumnInvoker<T> extends TableInvoker {
79         private final ColumnSchema<GenericTableSchema, T> columnSchema;
80
81         ColumnInvoker(final GenericTableSchema tableSchema, final ColumnSchema<GenericTableSchema, T> columnSchema) {
82             super(requireNonNull(tableSchema));
83             this.columnSchema = columnSchema;
84         }
85
86         @Override
87         final @NonNull GenericTableSchema tableSchema() {
88             return verifyNotNull(super.tableSchema());
89         }
90
91         @Nullable ColumnSchema<GenericTableSchema, T> columnSchema() {
92             return columnSchema;
93         }
94
95         @Override
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);
100         }
101
102         abstract Object invokeMethod(Object proxy, Object[] args);
103
104         abstract Object invokeRowMethod(@NonNull Row<GenericTableSchema> row, Object proxy, Object[] args);
105     }
106
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>() {
111                 @Override
112                 public MethodDispatch load(final Class<?> key) {
113                     return new MethodDispatch(key);
114                 }
115             });
116
117     private abstract static class VersionedPrototype extends Prototype {
118         private static final Logger LOG = LoggerFactory.getLogger(VersionedPrototype.class);
119
120         private final Range<Version> supportedVersions;
121
122         VersionedPrototype(final Range<Version> supportedVersions) {
123             this.supportedVersions = requireNonNull(supportedVersions);
124         }
125
126         @Override
127         final Invoker bindTo(final TypedDatabaseSchema dbSchema) {
128             final Version version = dbSchema.getVersion();
129             if (supportedVersions.contains(version)) {
130                 return bindToImpl(dbSchema);
131             }
132
133             LOG.debug("Version {} does not match required range {}, deferring failure to invocation time", version,
134                 supportedVersions);
135             return new FailedInvoker() {
136                 @Override
137                 RuntimeException newException() {
138                     return new SchemaVersionMismatchException(version, supportedVersions);
139                 }
140             };
141         }
142
143         abstract Invoker bindToImpl(TypedDatabaseSchema dbSchema);
144     }
145
146     abstract static class TablePrototype extends VersionedPrototype {
147         private static final Logger LOG = LoggerFactory.getLogger(TablePrototype.class);
148
149         private final String tableName;
150
151         TablePrototype(final Range<Version> supportedVersions, final String tableName) {
152             super(supportedVersions);
153             this.tableName = requireNonNull(tableName);
154         }
155
156         final @Nullable GenericTableSchema findTableSchema(final DatabaseSchema dbSchema) {
157             return dbSchema.table(tableName, GenericTableSchema.class);
158         }
159
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,
163                 dbName);
164             return new FailedInvoker() {
165                 @Override
166                 RuntimeException newException() {
167                     return new TableSchemaNotFoundException(tableName, dbName);
168                 }
169             };
170         }
171     }
172
173     abstract static class ColumnPrototype<T> extends TablePrototype {
174         private static final Logger LOG = LoggerFactory.getLogger(ColumnPrototype.class);
175
176         private final @NonNull Class<T> columnType;
177         private final @NonNull String columnName;
178
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);
184         }
185
186         final @NonNull String columnName() {
187             return columnName;
188         }
189
190         final @Nullable ColumnSchema<GenericTableSchema, T> findColumnSchema(
191                 final @NonNull GenericTableSchema tableSchema) {
192             return tableSchema.column(columnName, columnType);
193         }
194
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,
198                 tableName);
199             return new FailedInvoker() {
200                 @Override
201                 RuntimeException newException() {
202                     return new ColumnSchemaNotFoundException(columnName, tableName);
203                 }
204             };
205         }
206
207         @Override
208         final Invoker bindToImpl(final TypedDatabaseSchema dbSchema) {
209             final GenericTableSchema tableSchema = findTableSchema(dbSchema);
210             return tableSchema != null ? bindToImpl(tableSchema) : tableSchemaNotFound(dbSchema);
211         }
212
213         abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema);
214     }
215
216     abstract static class StrictColumnPrototype<T> extends ColumnPrototype<T> {
217
218         StrictColumnPrototype(final Method method, final String tableName, final String columnName) {
219             super(method, method.getReturnType(), tableName, columnName);
220         }
221
222         @Override
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);
226
227         }
228
229         abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema,
230                 @NonNull ColumnSchema<GenericTableSchema, T> columnSchema);
231     }
232
233     private final @NonNull ImmutableMap<Method, Prototype> prototypes;
234     private final @NonNull String tableName;
235
236     private MethodDispatch(final Class<?> key) {
237         tableName = TypedReflections.getTableName(key);
238
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);
244             }
245         }
246         prototypes = builder.build();
247     }
248
249     static MethodDispatch forTarget(final Class<?> target) {
250         return CACHE.getUnchecked(target);
251     }
252
253     @NonNull TypedRowInvocationHandler bindToSchema(final TypedDatabaseSchema dbSchema) {
254         return new TypedRowInvocationHandler(tableName,
255             ImmutableMap.copyOf(Maps.transformValues(prototypes, prototype -> prototype.bindTo(dbSchema))));
256     }
257
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) {
263                 case GETCOLUMN:
264                     return new GetColumn<>(method, tableName, typedColumn.name());
265                 case GETDATA:
266                     return new GetData<>(method, tableName, typedColumn.name());
267                 case GETROW:
268                     return GetRow.INSTANCE;
269                 case GETTABLESCHEMA:
270                     return new GetTable(tableName);
271                 case SETDATA:
272                     return new SetData<>(method, tableName, typedColumn.name());
273                 default:
274                     throw new TyperException("Unhandled method type " + methodType);
275             }
276         }
277
278         /*
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.
284          */
285         final String name = method.getName();
286         if (name.startsWith("set")) {
287             return new SetData<>(method, accessorName(name.substring(3)), tableName);
288         }
289         if (name.startsWith("get")) {
290             if (name.endsWith("Row")) {
291                 return GetRow.INSTANCE;
292             }
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)));
296             }
297             return new GetData<>(method, accessorName(tail), tableName);
298         }
299
300         return null;
301     }
302
303     private static String accessorName(final String columnName) {
304         return columnName.toLowerCase(Locale.ROOT);
305     }
306 }