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 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;
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.
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.
44 final class MethodDispatch {
45 abstract static class Invoker {
47 abstract Object invokeMethod(Row<GenericTableSchema> row, Object proxy, Object[] args);
50 abstract static class Prototype {
52 abstract Invoker bindTo(TypedDatabaseSchema dbSchema);
55 abstract static class FailedInvoker extends Invoker {
57 final Object invokeMethod(final Row<GenericTableSchema> row, final Object proxy, final Object[] args) {
61 abstract @NonNull RuntimeException newException();
64 abstract static class TableInvoker extends Invoker {
65 private final GenericTableSchema tableSchema;
67 TableInvoker(final GenericTableSchema tableSchema) {
68 this.tableSchema = tableSchema;
71 @Nullable GenericTableSchema tableSchema() {
76 abstract static class ColumnInvoker<T> extends TableInvoker {
77 private final ColumnSchema<GenericTableSchema, T> columnSchema;
79 ColumnInvoker(final GenericTableSchema tableSchema, final ColumnSchema<GenericTableSchema, T> columnSchema) {
80 super(requireNonNull(tableSchema));
81 this.columnSchema = columnSchema;
85 final @NonNull GenericTableSchema tableSchema() {
86 return verifyNotNull(super.tableSchema());
89 @Nullable ColumnSchema<GenericTableSchema, T> columnSchema() {
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);
100 abstract Object invokeMethod(Object proxy, Object[] args);
102 abstract Object invokeRowMethod(@NonNull Row<GenericTableSchema> row, Object proxy, Object[] args);
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>() {
110 public MethodDispatch load(final Class<?> key) {
111 return new MethodDispatch(key);
115 private abstract static class VersionedPrototype extends Prototype {
116 private static final Logger LOG = LoggerFactory.getLogger(VersionedPrototype.class);
118 private final Range<Version> supportedVersions;
120 VersionedPrototype(final Range<Version> supportedVersions) {
121 this.supportedVersions = requireNonNull(supportedVersions);
125 final Invoker bindTo(final TypedDatabaseSchema dbSchema) {
126 final Version version = dbSchema.getVersion();
127 if (supportedVersions.contains(version)) {
128 return bindToImpl(dbSchema);
131 LOG.debug("Version {} does not match required range {}, deferring failure to invocation time", version,
133 return new FailedInvoker() {
135 RuntimeException newException() {
136 return new SchemaVersionMismatchException(version, supportedVersions);
141 abstract Invoker bindToImpl(TypedDatabaseSchema dbSchema);
144 abstract static class TablePrototype extends VersionedPrototype {
145 private static final Logger LOG = LoggerFactory.getLogger(TablePrototype.class);
147 private final String tableName;
149 TablePrototype(final Range<Version> supportedVersions, final String tableName) {
150 super(supportedVersions);
151 this.tableName = requireNonNull(tableName);
154 final @Nullable GenericTableSchema findTableSchema(final DatabaseSchema dbSchema) {
155 return dbSchema.table(tableName, GenericTableSchema.class);
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,
162 return new FailedInvoker() {
164 RuntimeException newException() {
165 return new TableSchemaNotFoundException(tableName, dbName);
171 abstract static class ColumnPrototype<T> extends TablePrototype {
172 private static final Logger LOG = LoggerFactory.getLogger(ColumnPrototype.class);
174 private final @NonNull Class<T> columnType;
175 private final @NonNull String columnName;
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);
184 final @NonNull String columnName() {
188 final @Nullable ColumnSchema<GenericTableSchema, T> findColumnSchema(
189 final @NonNull GenericTableSchema tableSchema) {
190 return tableSchema.column(columnName, columnType);
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,
197 return new FailedInvoker() {
199 RuntimeException newException() {
200 return new ColumnSchemaNotFoundException(columnName, tableName);
206 final Invoker bindToImpl(final TypedDatabaseSchema dbSchema) {
207 final GenericTableSchema tableSchema = findTableSchema(dbSchema);
208 return tableSchema != null ? bindToImpl(tableSchema) : tableSchemaNotFound(dbSchema);
211 abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema);
214 abstract static class StrictColumnPrototype<T> extends ColumnPrototype<T> {
216 StrictColumnPrototype(final Method method, final String tableName, final String columnName) {
217 super(method, method.getReturnType(), tableName, columnName);
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);
227 abstract Invoker bindToImpl(@NonNull GenericTableSchema tableSchema,
228 @NonNull ColumnSchema<GenericTableSchema, T> columnSchema);
231 private final @NonNull ImmutableMap<Method, Prototype> prototypes;
232 private final @NonNull String tableName;
234 private MethodDispatch(final Class<?> key) {
235 tableName = TypedReflections.getTableName(key);
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);
244 this.prototypes = builder.build();
247 static MethodDispatch forTarget(final Class<?> target) {
248 return CACHE.getUnchecked(target);
251 @NonNull TypedRowInvocationHandler bindToSchema(final TypedDatabaseSchema dbSchema) {
252 return new TypedRowInvocationHandler(tableName,
253 ImmutableMap.copyOf(Maps.transformValues(prototypes, prototype -> prototype.bindTo(dbSchema))));
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) {
262 return new GetColumn<>(method, tableName, typedColumn.name());
264 return new GetData<>(method, tableName, typedColumn.name());
266 return GetRow.INSTANCE;
268 return new GetTable(tableName);
270 return new SetData<>(method, tableName, typedColumn.name());
272 throw new TyperException("Unhandled method type " + methodType);
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.
283 final String name = method.getName();
284 if (name.startsWith("set")) {
285 return new SetData<>(method, accessorName(name.substring(3)), tableName);
287 if (name.startsWith("get")) {
288 if (name.endsWith("Row")) {
289 return GetRow.INSTANCE;
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)));
295 return new GetData<>(method, accessorName(tail), tableName);
301 private static String accessorName(final String columnName) {
302 return columnName.toLowerCase(Locale.ROOT);