Use netconf-3.0.5
[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 java.lang.reflect.Method;
20 import java.util.Locale;
21 import org.eclipse.jdt.annotation.NonNull;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.opendaylight.ovsdb.lib.error.ColumnSchemaNotFoundException;
24 import org.opendaylight.ovsdb.lib.error.SchemaVersionMismatchException;
25 import org.opendaylight.ovsdb.lib.error.TableSchemaNotFoundException;
26 import org.opendaylight.ovsdb.lib.error.TyperException;
27 import org.opendaylight.ovsdb.lib.notation.Row;
28 import org.opendaylight.ovsdb.lib.notation.Version;
29 import org.opendaylight.ovsdb.lib.schema.ColumnSchema;
30 import org.opendaylight.ovsdb.lib.schema.DatabaseSchema;
31 import org.opendaylight.ovsdb.lib.schema.GenericTableSchema;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Table to Method runtime-constant support. The binding of Class methods to corresponding data operations is defined
37  * by annotations, which means that such mapping is Class-invariant. This invariance is captured in this class.
38  *
39  * <p>
40  * Data operations are always invoked in the context of a runtime {@link DatabaseSchema}, i.e. for a particular device
41  * or a device function. This class exposes {@link #bindToSchema(TypedDatabaseSchema)}, which will construct an
42  * immutable mapping between a Method and its invocation handler.
43  */
44 final class MethodDispatch {
45     abstract static class Invoker {
46
47         abstract Object invokeMethod(Row<GenericTableSchema> row, Object proxy, Object[] args);
48     }
49
50     abstract static class Prototype {
51
52         abstract Invoker bindTo(TypedDatabaseSchema dbSchema);
53     }
54
55     abstract static class FailedInvoker extends Invoker {
56         @Override
57         final Object invokeMethod(final Row<GenericTableSchema> row, final Object proxy, final Object[] args) {
58             throw newException();
59         }
60
61         abstract @NonNull RuntimeException newException();
62     }
63
64     abstract static class TableInvoker extends Invoker {
65         private final GenericTableSchema tableSchema;
66
67         TableInvoker(final GenericTableSchema tableSchema) {
68             this.tableSchema = tableSchema;
69         }
70
71         @Nullable GenericTableSchema tableSchema() {
72             return tableSchema;
73         }
74     }
75
76     abstract static class ColumnInvoker<T> extends TableInvoker {
77         private final ColumnSchema<GenericTableSchema, T> columnSchema;
78
79         ColumnInvoker(final GenericTableSchema tableSchema, final ColumnSchema<GenericTableSchema, T> columnSchema) {
80             super(requireNonNull(tableSchema));
81             this.columnSchema = columnSchema;
82         }
83
84         @Override
85         final @NonNull GenericTableSchema tableSchema() {
86             return verifyNotNull(super.tableSchema());
87         }
88
89         @Nullable ColumnSchema<GenericTableSchema, T> columnSchema() {
90             return columnSchema;
91         }
92
93         @Override
94         Object invokeMethod(final Row<GenericTableSchema> row, final Object proxy, final Object[] args) {
95             // When the row is null, that might indicate that the user maybe interested
96             // only in the ColumnSchema and not on the Data.
97             return row == null ? invokeMethod(proxy, args) : invokeRowMethod(row, proxy, args);
98         }
99
100         abstract Object invokeMethod(Object proxy, Object[] args);
101
102         abstract Object invokeRowMethod(@NonNull Row<GenericTableSchema> row, Object proxy, Object[] args);
103     }
104
105     // As the mode of invocation for a particular method is invariant, we keep the set of dynamically-supported method
106     // in a per-Class cache, thus skipping reflective operations at invocation time.
107     private static final LoadingCache<Class<?>, MethodDispatch> CACHE = CacheBuilder.newBuilder()
108             .weakKeys().weakValues().build(new CacheLoader<Class<?>, MethodDispatch>() {
109                 @Override
110                 public MethodDispatch load(final Class<?> key) {
111                     return new MethodDispatch(key);
112                 }
113             });
114
115     private abstract static class VersionedPrototype extends Prototype {
116         private static final Logger LOG = LoggerFactory.getLogger(VersionedPrototype.class);
117
118         private final Range<Version> supportedVersions;
119
120         VersionedPrototype(final Range<Version> supportedVersions) {
121             this.supportedVersions = requireNonNull(supportedVersions);
122         }
123
124         @Override
125         final Invoker bindTo(final TypedDatabaseSchema dbSchema) {
126             final Version version = dbSchema.getVersion();
127             if (supportedVersions.contains(version)) {
128                 return bindToImpl(dbSchema);
129             }
130
131             LOG.debug("Version {} does not match required range {}, deferring failure to invocation time", version,
132                 supportedVersions);
133             return new FailedInvoker() {
134                 @Override
135                 RuntimeException newException() {
136                     return new SchemaVersionMismatchException(version, supportedVersions);
137                 }
138             };
139         }
140
141         abstract Invoker bindToImpl(TypedDatabaseSchema dbSchema);
142     }
143
144     abstract static class TablePrototype extends VersionedPrototype {
145         private static final Logger LOG = LoggerFactory.getLogger(TablePrototype.class);
146
147         private final String tableName;
148
149         TablePrototype(final Range<Version> supportedVersions, final String tableName) {
150             super(supportedVersions);
151             this.tableName = requireNonNull(tableName);
152         }
153
154         final @Nullable GenericTableSchema findTableSchema(final DatabaseSchema dbSchema) {
155             return dbSchema.table(tableName, GenericTableSchema.class);
156         }
157
158         final @NonNull FailedInvoker tableSchemaNotFound(final DatabaseSchema dbSchema) {
159             final String dbName = dbSchema.getName();
160             LOG.debug("Failed to find schema for table {} in {}, deferring failure to invocation time", tableName,
161                 dbName);
162             return new FailedInvoker() {
163                 @Override
164                 RuntimeException newException() {
165                     return new TableSchemaNotFoundException(tableName, dbName);
166                 }
167             };
168         }
169     }
170
171     abstract static class ColumnPrototype<T> extends TablePrototype {
172         private static final Logger LOG = LoggerFactory.getLogger(ColumnPrototype.class);
173
174         private final @NonNull Class<T> columnType;
175         private final @NonNull String columnName;
176
177         ColumnPrototype(final Method method, final Class<?> columnType, final String tableName,
178                 final String columnName) {
179             super(TypedReflections.getColumnVersionRange(method), tableName);
180             this.columnName = requireNonNull(columnName);
181             this.columnType = requireNonNull((Class<T>) columnType);
182         }
183
184         final @NonNull String columnName() {
185             return columnName;
186         }
187
188         final @Nullable ColumnSchema<GenericTableSchema, T> findColumnSchema(
189                 final @NonNull GenericTableSchema tableSchema) {
190             return tableSchema.column(columnName, columnType);
191         }
192
193         final @NonNull FailedInvoker columnSchemaNotFound(final @NonNull GenericTableSchema tableSchema) {
194             final String tableName = tableSchema.getName();
195             LOG.debug("Failed to find schema for column {} in {}, deferring failure to invocation time", columnName,
196                 tableName);
197             return new FailedInvoker() {
198                 @Override
199                 RuntimeException newException() {
200                     return new ColumnSchemaNotFoundException(columnName, tableName);
201                 }
202             };
203         }
204
205         @Override
206         final Invoker bindToImpl(final TypedDatabaseSchema dbSchema) {
207             final GenericTableSchema tableSchema = findTableSchema(dbSchema);
208             return tableSchema != null ? bindToImpl(tableSchema) : tableSchemaNotFound(dbSchema);
209         }
210
211         abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema);
212     }
213
214     abstract static class StrictColumnPrototype<T> extends ColumnPrototype<T> {
215
216         StrictColumnPrototype(final Method method, final String tableName, final String columnName) {
217             super(method, method.getReturnType(), tableName, columnName);
218         }
219
220         @Override
221         final Invoker bindToImpl(@NonNull final GenericTableSchema tableSchema) {
222             final ColumnSchema<GenericTableSchema, T> columnSchema = findColumnSchema(tableSchema);
223             return columnSchema != null ? bindToImpl(tableSchema, columnSchema) : columnSchemaNotFound(tableSchema);
224
225         }
226
227         abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema,
228                 @NonNull ColumnSchema<GenericTableSchema, T> columnSchema);
229     }
230
231     private final @NonNull ImmutableMap<Method, Prototype> prototypes;
232     private final @NonNull String tableName;
233
234     private MethodDispatch(final Class<?> key) {
235         tableName = TypedReflections.getTableName(key);
236
237         final ImmutableMap.Builder<Method, Prototype> builder = ImmutableMap.builder();
238         for (Method method : key.getMethods()) {
239             final Prototype prototype = MethodDispatch.prototypeFor(tableName, method);
240             if (prototype != null) {
241                 builder.put(method, prototype);
242             }
243         }
244         this.prototypes = builder.build();
245     }
246
247     static MethodDispatch forTarget(final Class<?> target) {
248         return CACHE.getUnchecked(target);
249     }
250
251     @NonNull TypedRowInvocationHandler bindToSchema(final TypedDatabaseSchema dbSchema) {
252         return new TypedRowInvocationHandler(tableName,
253             ImmutableMap.copyOf(Maps.transformValues(prototypes, prototype -> prototype.bindTo(dbSchema))));
254     }
255
256     private static @Nullable Prototype prototypeFor(final String tableName, final Method method) {
257         final TypedColumn typedColumn = method.getAnnotation(TypedColumn.class);
258         if (typedColumn != null) {
259             final MethodType methodType = typedColumn.method();
260             switch (methodType) {
261                 case GETCOLUMN:
262                     return new GetColumn<>(method, tableName, typedColumn.name());
263                 case GETDATA:
264                     return new GetData<>(method, tableName, typedColumn.name());
265                 case GETROW:
266                     return GetRow.INSTANCE;
267                 case GETTABLESCHEMA:
268                     return new GetTable(tableName);
269                 case SETDATA:
270                     return new SetData<>(method, tableName, typedColumn.name());
271                 default:
272                     throw new TyperException("Unhandled method type " + methodType);
273             }
274         }
275
276         /*
277          * Attempting to get the column name by parsing the method name with a following convention :
278          * 1. GETDATA : get<ColumnName>
279          * 2. SETDATA : set<ColumnName>
280          * 3. GETCOLUMN : get<ColumnName>Column
281          * where <ColumnName> is the name of the column that we are interested in.
282          */
283         final String name = method.getName();
284         if (name.startsWith("set")) {
285             return new SetData<>(method, accessorName(name.substring(3)), tableName);
286         }
287         if (name.startsWith("get")) {
288             if (name.endsWith("Row")) {
289                 return GetRow.INSTANCE;
290             }
291             final String tail = name.substring(3);
292             if (tail.endsWith("Column")) {
293                 return new GetColumn<>(method, tableName, accessorName(tail.substring(0, tail.length() - 6)));
294             }
295             return new GetData<>(method, accessorName(tail), tableName);
296         }
297
298         return null;
299     }
300
301     private static String accessorName(final String columnName) {
302         return columnName.toLowerCase(Locale.ROOT);
303     }
304 }