blob: d5bf2e675f59bfef1a78a634c5624a3097c2682b [file] [log] [blame]
Carmelo Cascone87892e22017-11-13 16:01:29 -08001/*
2 * Copyright 2017-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.p4runtime.model;
18
19import com.google.common.collect.ImmutableList;
20import com.google.common.collect.ImmutableMap;
21import com.google.common.collect.ImmutableSet;
22import com.google.common.collect.Maps;
23import com.google.protobuf.ExtensionRegistry;
24import com.google.protobuf.TextFormat;
25import org.onosproject.net.pi.model.PiActionId;
26import org.onosproject.net.pi.model.PiActionModel;
27import org.onosproject.net.pi.model.PiActionParamId;
28import org.onosproject.net.pi.model.PiActionParamModel;
29import org.onosproject.net.pi.model.PiActionProfileId;
30import org.onosproject.net.pi.model.PiActionProfileModel;
Carmelo Cascone87892e22017-11-13 16:01:29 -080031import org.onosproject.net.pi.model.PiCounterId;
32import org.onosproject.net.pi.model.PiCounterModel;
33import org.onosproject.net.pi.model.PiCounterType;
34import org.onosproject.net.pi.model.PiMatchFieldId;
35import org.onosproject.net.pi.model.PiMatchFieldModel;
36import org.onosproject.net.pi.model.PiMatchType;
37import org.onosproject.net.pi.model.PiMeterId;
38import org.onosproject.net.pi.model.PiMeterModel;
39import org.onosproject.net.pi.model.PiMeterType;
Carmelo Casconea3635ab2019-03-19 12:55:34 -070040import org.onosproject.net.pi.model.PiPacketMetadataId;
41import org.onosproject.net.pi.model.PiPacketMetadataModel;
Carmelo Cascone87892e22017-11-13 16:01:29 -080042import org.onosproject.net.pi.model.PiPacketOperationModel;
43import org.onosproject.net.pi.model.PiPacketOperationType;
44import org.onosproject.net.pi.model.PiPipelineModel;
FrankWang2674e452018-05-24 17:13:35 +080045import org.onosproject.net.pi.model.PiRegisterId;
46import org.onosproject.net.pi.model.PiRegisterModel;
Carmelo Cascone87892e22017-11-13 16:01:29 -080047import org.onosproject.net.pi.model.PiTableId;
48import org.onosproject.net.pi.model.PiTableModel;
49import org.onosproject.net.pi.model.PiTableType;
Carmelo Casconea3635ab2019-03-19 12:55:34 -070050import org.slf4j.Logger;
Carmelo Cascone6af4e172018-06-15 16:01:30 +020051import p4.config.v1.P4InfoOuterClass;
52import p4.config.v1.P4InfoOuterClass.Action;
53import p4.config.v1.P4InfoOuterClass.ActionProfile;
54import p4.config.v1.P4InfoOuterClass.ActionRef;
55import p4.config.v1.P4InfoOuterClass.ControllerPacketMetadata;
56import p4.config.v1.P4InfoOuterClass.Counter;
57import p4.config.v1.P4InfoOuterClass.CounterSpec;
58import p4.config.v1.P4InfoOuterClass.DirectCounter;
59import p4.config.v1.P4InfoOuterClass.DirectMeter;
60import p4.config.v1.P4InfoOuterClass.MatchField;
61import p4.config.v1.P4InfoOuterClass.Meter;
62import p4.config.v1.P4InfoOuterClass.MeterSpec;
63import p4.config.v1.P4InfoOuterClass.P4Info;
64import p4.config.v1.P4InfoOuterClass.Table;
Carmelo Cascone87892e22017-11-13 16:01:29 -080065
66import java.io.IOException;
67import java.io.InputStream;
68import java.io.InputStreamReader;
69import java.net.URL;
70import java.util.Map;
71import java.util.Objects;
72import java.util.stream.Collectors;
73
74import static java.lang.String.format;
Carmelo Casconea3635ab2019-03-19 12:55:34 -070075import static org.slf4j.LoggerFactory.getLogger;
Carmelo Cascone87892e22017-11-13 16:01:29 -080076
77/**
78 * Parser of P4Info to PI pipeline model instances.
79 */
80public final class P4InfoParser {
81
Carmelo Casconea3635ab2019-03-19 12:55:34 -070082 private static final Logger log = getLogger(P4InfoParser.class);
83
Carmelo Cascone87892e22017-11-13 16:01:29 -080084 private static final String PACKET_IN = "packet_in";
85 private static final String PACKET_OUT = "packet_out";
86
87 private static final Map<CounterSpec.Unit, PiCounterModel.Unit> COUNTER_UNIT_MAP =
88 new ImmutableMap.Builder<CounterSpec.Unit, PiCounterModel.Unit>()
89 .put(CounterSpec.Unit.BYTES, PiCounterModel.Unit.BYTES)
90 .put(CounterSpec.Unit.PACKETS, PiCounterModel.Unit.PACKETS)
91 .put(CounterSpec.Unit.BOTH, PiCounterModel.Unit.PACKETS_AND_BYTES)
92 // Don't map UNSPECIFIED as we don't support it at the moment.
93 .build();
94
95 private static final Map<MeterSpec.Unit, PiMeterModel.Unit> METER_UNIT_MAP =
96 new ImmutableMap.Builder<MeterSpec.Unit, PiMeterModel.Unit>()
97 .put(MeterSpec.Unit.BYTES, PiMeterModel.Unit.BYTES)
98 .put(MeterSpec.Unit.PACKETS, PiMeterModel.Unit.PACKETS)
99 // Don't map UNSPECIFIED as we don't support it at the moment.
100 .build();
101
102 private static final Map<String, PiPacketOperationType> PACKET_OPERATION_TYPE_MAP =
103 new ImmutableMap.Builder<String, PiPacketOperationType>()
104 .put(PACKET_IN, PiPacketOperationType.PACKET_IN)
105 .put(PACKET_OUT, PiPacketOperationType.PACKET_OUT)
106 .build();
107
108 private static final Map<MatchField.MatchType, PiMatchType> MATCH_TYPE_MAP =
109 new ImmutableMap.Builder<MatchField.MatchType, PiMatchType>()
Carmelo Cascone87892e22017-11-13 16:01:29 -0800110 .put(MatchField.MatchType.EXACT, PiMatchType.EXACT)
111 .put(MatchField.MatchType.LPM, PiMatchType.LPM)
112 .put(MatchField.MatchType.TERNARY, PiMatchType.TERNARY)
113 .put(MatchField.MatchType.RANGE, PiMatchType.RANGE)
114 // Don't map UNSPECIFIED as we don't support it at the moment.
115 .build();
116 public static final int NO_SIZE = -1;
117
118 private P4InfoParser() {
119 // Utility class, hides constructor.
120 }
121
122 /**
123 * Parse the given URL pointing to a P4Info file (in text format) to a PI pipeline model.
124 *
125 * @param p4InfoUrl URL to P4Info in text form
126 * @return PI pipeline model
127 * @throws P4InfoParserException if the P4Info file cannot be parsed (see message)
128 */
129 public static PiPipelineModel parse(URL p4InfoUrl) throws P4InfoParserException {
130
131 final P4Info p4info;
132 try {
133 p4info = getP4InfoMessage(p4InfoUrl);
134 } catch (IOException e) {
135 throw new P4InfoParserException("Unable to parse protobuf " + p4InfoUrl.toString(), e);
136 }
137
138 // Start by parsing and mapping instances to to their integer P4Info IDs.
139 // Convenient to build the table model at the end.
140
141 // Counters.
142 final Map<Integer, PiCounterModel> counterMap = Maps.newHashMap();
143 counterMap.putAll(parseCounters(p4info));
144 counterMap.putAll(parseDirectCounters(p4info));
145
146 // Meters.
147 final Map<Integer, PiMeterModel> meterMap = Maps.newHashMap();
148 meterMap.putAll(parseMeters(p4info));
149 meterMap.putAll(parseDirectMeters(p4info));
150
FrankWang2674e452018-05-24 17:13:35 +0800151 // Registers.
152 final Map<Integer, PiRegisterModel> registerMap = Maps.newHashMap();
153 registerMap.putAll(parseRegisters(p4info));
154
Carmelo Cascone87892e22017-11-13 16:01:29 -0800155 // Action profiles.
156 final Map<Integer, PiActionProfileModel> actProfileMap = parseActionProfiles(p4info);
157
158 // Actions.
159 final Map<Integer, PiActionModel> actionMap = parseActions(p4info);
160
161 // Controller packet metadatas.
162 final Map<PiPacketOperationType, PiPacketOperationModel> pktOpMap = parseCtrlPktMetadatas(p4info);
163
164 // Finally, parse tables.
165 final ImmutableMap.Builder<PiTableId, PiTableModel> tableImmMapBuilder =
166 ImmutableMap.builder();
167 for (Table tableMsg : p4info.getTablesList()) {
168 final PiTableId tableId = PiTableId.of(tableMsg.getPreamble().getName());
169 // Parse match fields.
170 final ImmutableMap.Builder<PiMatchFieldId, PiMatchFieldModel> tableFieldMapBuilder =
171 ImmutableMap.builder();
172 for (MatchField fieldMsg : tableMsg.getMatchFieldsList()) {
173 final PiMatchFieldId fieldId = PiMatchFieldId.of(fieldMsg.getName());
174 tableFieldMapBuilder.put(
175 fieldId,
176 new P4MatchFieldModel(fieldId,
177 fieldMsg.getBitwidth(),
178 mapMatchFieldType(fieldMsg.getMatchType())));
179
180 }
181 // Retrieve action models by inter IDs.
182 final ImmutableMap.Builder<PiActionId, PiActionModel> tableActionMapBuilder =
183 ImmutableMap.builder();
184 tableMsg.getActionRefsList().stream()
185 .map(ActionRef::getId)
186 .map(actionMap::get)
187 .forEach(actionModel -> tableActionMapBuilder.put(actionModel.id(), actionModel));
188 // Retrieve direct meters by integer IDs.
189 final ImmutableMap.Builder<PiMeterId, PiMeterModel> tableMeterMapBuilder =
190 ImmutableMap.builder();
191 tableMsg.getDirectResourceIdsList()
192 .stream()
193 .map(meterMap::get)
194 // Direct resource ID might be that of a counter.
195 // Filter out missed mapping.
196 .filter(Objects::nonNull)
197 .forEach(meterModel -> tableMeterMapBuilder.put(meterModel.id(), meterModel));
198 // Retrieve direct counters by integer IDs.
199 final ImmutableMap.Builder<PiCounterId, PiCounterModel> tableCounterMapBuilder =
200 ImmutableMap.builder();
201 tableMsg.getDirectResourceIdsList()
202 .stream()
203 .map(counterMap::get)
204 // As before, resource ID might be that of a meter.
205 // Filter out missed mapping.
206 .filter(Objects::nonNull)
207 .forEach(counterModel -> tableCounterMapBuilder.put(counterModel.id(), counterModel));
208 tableImmMapBuilder.put(
209 tableId,
210 new P4TableModel(
211 PiTableId.of(tableMsg.getPreamble().getName()),
212 tableMsg.getImplementationId() == 0 ? PiTableType.DIRECT : PiTableType.INDIRECT,
213 actProfileMap.get(tableMsg.getImplementationId()),
214 tableMsg.getSize(),
215 tableCounterMapBuilder.build(),
216 tableMeterMapBuilder.build(),
Carmelo Cascone6af4e172018-06-15 16:01:30 +0200217 !tableMsg.getIdleTimeoutBehavior()
218 .equals(Table.IdleTimeoutBehavior.NO_TIMEOUT),
Carmelo Cascone87892e22017-11-13 16:01:29 -0800219 tableFieldMapBuilder.build(),
220 tableActionMapBuilder.build(),
221 actionMap.get(tableMsg.getConstDefaultActionId()),
Carmelo Cascone33b27bc2018-09-09 22:56:14 -0700222 tableMsg.getIsConstTable()));
Carmelo Cascone87892e22017-11-13 16:01:29 -0800223
224 }
225
226 // Get a map with proper PI IDs for some of those maps we created at the beginning.
227 ImmutableMap<PiCounterId, PiCounterModel> counterImmMap = ImmutableMap.copyOf(
228 counterMap.values().stream()
229 .collect(Collectors.toMap(PiCounterModel::id, c -> c)));
230 ImmutableMap<PiMeterId, PiMeterModel> meterImmMap = ImmutableMap.copyOf(
231 meterMap.values().stream()
232 .collect(Collectors.toMap(PiMeterModel::id, m -> m)));
FrankWang2674e452018-05-24 17:13:35 +0800233 ImmutableMap<PiRegisterId, PiRegisterModel> registerImmMap = ImmutableMap.copyOf(
234 registerMap.values().stream()
235 .collect(Collectors.toMap(PiRegisterModel::id, r -> r)));
Carmelo Cascone87892e22017-11-13 16:01:29 -0800236 ImmutableMap<PiActionProfileId, PiActionProfileModel> actProfileImmMap = ImmutableMap.copyOf(
237 actProfileMap.values().stream()
238 .collect(Collectors.toMap(PiActionProfileModel::id, a -> a)));
239
240 return new P4PipelineModel(
241 tableImmMapBuilder.build(),
242 counterImmMap,
243 meterImmMap,
FrankWang2674e452018-05-24 17:13:35 +0800244 registerImmMap,
Carmelo Cascone87892e22017-11-13 16:01:29 -0800245 actProfileImmMap,
246 ImmutableMap.copyOf(pktOpMap));
247 }
248
249
250 private static Map<Integer, PiCounterModel> parseCounters(P4Info p4info)
251 throws P4InfoParserException {
252 final Map<Integer, PiCounterModel> counterMap = Maps.newHashMap();
253 for (Counter counterMsg : p4info.getCountersList()) {
254 counterMap.put(
255 counterMsg.getPreamble().getId(),
256 new P4CounterModel(
257 PiCounterId.of(counterMsg.getPreamble().getName()),
258 PiCounterType.INDIRECT,
259 mapCounterSpecUnit(counterMsg.getSpec()),
260 null,
261 counterMsg.getSize()));
262 }
263 return counterMap;
264 }
265
266 private static Map<Integer, PiCounterModel> parseDirectCounters(P4Info p4info)
267 throws P4InfoParserException {
268 final Map<Integer, PiCounterModel> counterMap = Maps.newHashMap();
269 for (DirectCounter dirCounterMsg : p4info.getDirectCountersList()) {
270 counterMap.put(
271 dirCounterMsg.getPreamble().getId(),
272 new P4CounterModel(
273 PiCounterId.of(dirCounterMsg.getPreamble().getName()),
274 PiCounterType.DIRECT,
275 mapCounterSpecUnit(dirCounterMsg.getSpec()),
276 PiTableId.of(getTableName(dirCounterMsg.getDirectTableId(), p4info)),
277 NO_SIZE));
278 }
279 return counterMap;
280 }
281
282 private static Map<Integer, PiMeterModel> parseMeters(P4Info p4info)
283 throws P4InfoParserException {
284 final Map<Integer, PiMeterModel> meterMap = Maps.newHashMap();
285 for (Meter meterMsg : p4info.getMetersList()) {
286 meterMap.put(
287 meterMsg.getPreamble().getId(),
288 new P4MeterModel(
289 PiMeterId.of(meterMsg.getPreamble().getName()),
290 PiMeterType.INDIRECT,
291 mapMeterSpecUnit(meterMsg.getSpec()),
292 null,
293 meterMsg.getSize()));
294 }
295 return meterMap;
296 }
297
298 private static Map<Integer, PiMeterModel> parseDirectMeters(P4Info p4info)
299 throws P4InfoParserException {
300 final Map<Integer, PiMeterModel> meterMap = Maps.newHashMap();
301 for (DirectMeter dirMeterMsg : p4info.getDirectMetersList()) {
302 meterMap.put(
303 dirMeterMsg.getPreamble().getId(),
304 new P4MeterModel(
305 PiMeterId.of(dirMeterMsg.getPreamble().getName()),
306 PiMeterType.DIRECT,
307 mapMeterSpecUnit(dirMeterMsg.getSpec()),
308 PiTableId.of(getTableName(dirMeterMsg.getDirectTableId(), p4info)),
309 NO_SIZE));
310 }
311 return meterMap;
312 }
313
Carmelo Cascone6af4e172018-06-15 16:01:30 +0200314 private static Map<Integer, PiRegisterModel> parseRegisters(P4Info p4info) {
FrankWang2674e452018-05-24 17:13:35 +0800315 final Map<Integer, PiRegisterModel> registerMap = Maps.newHashMap();
316 for (P4InfoOuterClass.Register registerMsg : p4info.getRegistersList()) {
317 registerMap.put(registerMsg.getPreamble().getId(),
318 new P4RegisterModel(PiRegisterId.of(registerMsg.getPreamble().getName()),
319 registerMsg.getSize()));
320 }
321 return registerMap;
322 }
323
Carmelo Cascone87892e22017-11-13 16:01:29 -0800324 private static Map<Integer, PiActionProfileModel> parseActionProfiles(P4Info p4info)
325 throws P4InfoParserException {
326 final Map<Integer, PiActionProfileModel> actProfileMap = Maps.newHashMap();
327 for (ActionProfile actProfileMsg : p4info.getActionProfilesList()) {
328 final ImmutableSet.Builder<PiTableId> tableIdSetBuilder = ImmutableSet.builder();
329 for (int tableId : actProfileMsg.getTableIdsList()) {
330 tableIdSetBuilder.add(PiTableId.of(getTableName(tableId, p4info)));
331 }
Carmelo Casconea3635ab2019-03-19 12:55:34 -0700332 // TODO: we should copy all annotations to model classes for later
333 // use in the PI framework.
334 // This is a temporary workaround to the inability of p4c to
335 // correctly interpret P4Runtime-defined max_group_size annotation:
336 // https://s3-us-west-2.amazonaws.com/p4runtime/docs/master/
337 // P4Runtime-Spec.html#sec-p4info-action-profile
338 final String maxSizeAnnString = findAnnotation(
339 "max_group_size", actProfileMsg.getPreamble());
340 final int maxSizeAnn = maxSizeAnnString != null
341 ? Integer.valueOf(maxSizeAnnString) : 0;
342 final int maxGroupSize;
343 if (actProfileMsg.getMaxGroupSize() == 0 && maxSizeAnn != 0) {
344 log.warn("Found valid 'max_group_size' annotation for " +
345 "ActionProfile {}, using that...",
346 actProfileMsg.getPreamble().getName());
347 maxGroupSize = maxSizeAnn;
348 } else {
349 maxGroupSize = actProfileMsg.getMaxGroupSize();
350 }
351
Carmelo Cascone87892e22017-11-13 16:01:29 -0800352 actProfileMap.put(
353 actProfileMsg.getPreamble().getId(),
354 new P4ActionProfileModel(
355 PiActionProfileId.of(actProfileMsg.getPreamble().getName()),
356 tableIdSetBuilder.build(),
357 actProfileMsg.getWithSelector(),
Carmelo Cascone99c59db2019-01-17 15:39:35 -0800358 actProfileMsg.getSize(),
Carmelo Casconea3635ab2019-03-19 12:55:34 -0700359 maxGroupSize));
Carmelo Cascone87892e22017-11-13 16:01:29 -0800360 }
361 return actProfileMap;
362 }
363
364 private static Map<Integer, PiActionModel> parseActions(P4Info p4info) {
365 final Map<Integer, PiActionModel> actionMap = Maps.newHashMap();
366 for (Action actionMsg : p4info.getActionsList()) {
367 final ImmutableMap.Builder<PiActionParamId, PiActionParamModel> paramMapBuilder =
368 ImmutableMap.builder();
369 actionMsg.getParamsList().forEach(paramMsg -> {
370 final PiActionParamId paramId = PiActionParamId.of(paramMsg.getName());
371 paramMapBuilder.put(paramId,
372 new P4ActionParamModel(PiActionParamId.of(paramMsg.getName()),
373 paramMsg.getBitwidth()));
374 });
375 actionMap.put(
376 actionMsg.getPreamble().getId(),
377 new P4ActionModel(
378 PiActionId.of(actionMsg.getPreamble().getName()),
379 paramMapBuilder.build()));
380
381 }
382 return actionMap;
383 }
384
385 private static Map<PiPacketOperationType, PiPacketOperationModel> parseCtrlPktMetadatas(P4Info p4info)
386 throws P4InfoParserException {
387 final Map<PiPacketOperationType, PiPacketOperationModel> packetOpMap = Maps.newHashMap();
388 for (ControllerPacketMetadata ctrlPktMetaMsg : p4info.getControllerPacketMetadataList()) {
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800389 final ImmutableList.Builder<PiPacketMetadataModel> metadataListBuilder =
Carmelo Cascone87892e22017-11-13 16:01:29 -0800390 ImmutableList.builder();
391 ctrlPktMetaMsg.getMetadataList().forEach(metadataMsg -> metadataListBuilder.add(
Carmelo Cascone4c289b72019-01-22 15:30:45 -0800392 new P4PacketMetadataModel(PiPacketMetadataId.of(metadataMsg.getName()),
Carmelo Cascone87892e22017-11-13 16:01:29 -0800393 metadataMsg.getBitwidth())));
394 packetOpMap.put(
395 mapPacketOpType(ctrlPktMetaMsg.getPreamble().getName()),
396 new P4PacketOperationModel(mapPacketOpType(ctrlPktMetaMsg.getPreamble().getName()),
397 metadataListBuilder.build()));
398
399 }
400 return packetOpMap;
401 }
402
403 private static P4Info getP4InfoMessage(URL p4InfoUrl) throws IOException {
404 InputStream p4InfoStream = p4InfoUrl.openStream();
405 P4Info.Builder p4InfoBuilder = P4Info.newBuilder();
406 TextFormat.getParser().merge(new InputStreamReader(p4InfoStream),
407 ExtensionRegistry.getEmptyRegistry(),
408 p4InfoBuilder);
409 return p4InfoBuilder.build();
410 }
411
412 private static String getTableName(int id, P4Info p4info)
413 throws P4InfoParserException {
414 return p4info.getTablesList().stream()
415 .filter(t -> t.getPreamble().getId() == id)
416 .findFirst()
417 .orElseThrow(() -> new P4InfoParserException(format(
418 "Not such table with ID %d", id)))
419 .getPreamble()
420 .getName();
421 }
422
423 private static PiCounterModel.Unit mapCounterSpecUnit(CounterSpec spec)
424 throws P4InfoParserException {
425 if (!COUNTER_UNIT_MAP.containsKey(spec.getUnit())) {
426 throw new P4InfoParserException(format(
427 "Unrecognized counter unit '%s'", spec.getUnit()));
428 }
429 return COUNTER_UNIT_MAP.get(spec.getUnit());
430 }
431
432 private static PiMeterModel.Unit mapMeterSpecUnit(MeterSpec spec)
433 throws P4InfoParserException {
434 if (!METER_UNIT_MAP.containsKey(spec.getUnit())) {
435 throw new P4InfoParserException(format(
436 "Unrecognized meter unit '%s'", spec.getUnit()));
437 }
438 return METER_UNIT_MAP.get(spec.getUnit());
439 }
440
441 private static PiPacketOperationType mapPacketOpType(String name)
442 throws P4InfoParserException {
443 if (!PACKET_OPERATION_TYPE_MAP.containsKey(name)) {
444 throw new P4InfoParserException(format(
445 "Unrecognized controller packet metadata name '%s'", name));
446 }
447 return PACKET_OPERATION_TYPE_MAP.get(name);
448 }
449
450 private static PiMatchType mapMatchFieldType(MatchField.MatchType type)
451 throws P4InfoParserException {
452 if (!MATCH_TYPE_MAP.containsKey(type)) {
453 throw new P4InfoParserException(format(
454 "Unrecognized match field type '%s'", type));
455 }
456 return MATCH_TYPE_MAP.get(type);
457 }
Carmelo Casconea3635ab2019-03-19 12:55:34 -0700458
459 private static String findAnnotation(String name, P4InfoOuterClass.Preamble preamble) {
460 return preamble.getAnnotationsList().stream()
461 .filter(a -> a.startsWith("@" + name))
462 // e.g. @my_annotaion(value)
463 .map(a -> a.substring(name.length() + 2, a.length() - 1))
464 .findFirst()
465 .orElse(null);
466 }
Carmelo Cascone87892e22017-11-13 16:01:29 -0800467}