blob: c0fd84635dbb626bf366adb0ec8bc37c75858710 [file] [log] [blame]
Carmelo Cascone00a59962017-06-16 17:51:49 +09001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2017-present Open Networking Foundation
Carmelo Cascone00a59962017-06-16 17:51:49 +09003 *
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.net.pi.impl;
18
19import com.google.common.collect.Maps;
20import com.google.common.collect.Sets;
21import org.onlab.util.ImmutableByteSequence;
22import org.onosproject.net.Device;
23import org.onosproject.net.flow.FlowRule;
Carmelo Cascone00a59962017-06-16 17:51:49 +090024import org.onosproject.net.flow.TrafficSelector;
25import org.onosproject.net.flow.TrafficTreatment;
26import org.onosproject.net.flow.criteria.Criterion;
27import org.onosproject.net.flow.criteria.PiCriterion;
28import org.onosproject.net.flow.instructions.Instruction;
29import org.onosproject.net.flow.instructions.PiInstruction;
30import org.onosproject.net.pi.model.PiActionModel;
31import org.onosproject.net.pi.model.PiActionParamModel;
32import org.onosproject.net.pi.model.PiPipeconf;
33import org.onosproject.net.pi.model.PiPipelineInterpreter;
34import org.onosproject.net.pi.model.PiPipelineModel;
35import org.onosproject.net.pi.model.PiTableMatchFieldModel;
36import org.onosproject.net.pi.model.PiTableModel;
37import org.onosproject.net.pi.runtime.PiAction;
38import org.onosproject.net.pi.runtime.PiActionParam;
39import org.onosproject.net.pi.runtime.PiExactFieldMatch;
40import org.onosproject.net.pi.runtime.PiFieldMatch;
41import org.onosproject.net.pi.runtime.PiHeaderFieldId;
42import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
Carmelo Cascone0e896a02017-07-31 07:22:27 +020043import org.onosproject.net.pi.runtime.PiMatchKey;
Carmelo Cascone00a59962017-06-16 17:51:49 +090044import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
45import org.onosproject.net.pi.runtime.PiTableAction;
46import org.onosproject.net.pi.runtime.PiTableEntry;
47import org.onosproject.net.pi.runtime.PiTableId;
48import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
49import org.slf4j.Logger;
50import org.slf4j.LoggerFactory;
51
52import java.util.Collection;
53import java.util.Map;
54import java.util.Optional;
55import java.util.Set;
56import java.util.StringJoiner;
57
58import static java.lang.String.format;
59import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
60import static org.onlab.util.ImmutableByteSequence.fit;
61import static org.onosproject.net.flow.criteria.Criterion.Type.PROTOCOL_INDEPENDENT;
62import static org.onosproject.net.pi.impl.CriterionTranslatorHelper.translateCriterion;
Carmelo Cascone87b9b392017-10-02 18:33:20 +020063import static org.onosproject.net.pi.impl.PiUtils.getInterpreterOrNull;
64import static org.onosproject.net.pi.impl.PiUtils.translateTableId;
65import static org.onosproject.net.pi.runtime.PiTranslationService.PiTranslationException;
Carmelo Cascone00a59962017-06-16 17:51:49 +090066
67/**
68 * Implementation of flow rule translation logic.
69 */
70final class PiFlowRuleTranslator {
71
Carmelo Casconea3d811c2017-08-22 01:03:45 +020072 public static final int MAX_PI_PRIORITY = (int) Math.pow(2, 24);
Carmelo Cascone87b9b392017-10-02 18:33:20 +020073 private static final Logger log = LoggerFactory.getLogger(PiFlowRuleTranslator.class);
Carmelo Cascone00a59962017-06-16 17:51:49 +090074
75 private PiFlowRuleTranslator() {
76 // Hide constructor.
77 }
78
Carmelo Cascone87b9b392017-10-02 18:33:20 +020079 /**
80 * Returns a PI table entry equivalent to the given flow rule, for the given pipeconf and device.
81 *
82 * @param rule flow rule
83 * @param pipeconf pipeconf
84 * @param device device
85 * @return PI table entry
86 * @throws PiTranslationException if the flow rule cannot be translated
87 */
88 static PiTableEntry translate(FlowRule rule, PiPipeconf pipeconf, Device device)
89 throws PiTranslationException {
Carmelo Cascone00a59962017-06-16 17:51:49 +090090
91 PiPipelineModel pipelineModel = pipeconf.pipelineModel();
92
93 // Retrieve interpreter, if any.
Carmelo Cascone87b9b392017-10-02 18:33:20 +020094 final PiPipelineInterpreter interpreter = getInterpreterOrNull(device, pipeconf);
95 // Get table model.
96 final PiTableId piTableId = translateTableId(rule.table(), interpreter);
97 final PiTableModel tableModel = getTableModel(piTableId, pipelineModel);
98 // Translate selector.
99 final Collection<PiFieldMatch> fieldMatches = translateFieldMatches(interpreter, rule.selector(), tableModel);
100 // Translate treatment.
101 final PiTableAction piTableAction = translateTreatment(rule.treatment(), interpreter, piTableId, pipelineModel);
Carmelo Casconea62ac3d2017-08-30 03:19:00 +0200102
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200103 // Build PI entry.
104 final PiTableEntry.Builder tableEntryBuilder = PiTableEntry.builder();
Carmelo Cascone00a59962017-06-16 17:51:49 +0900105
Carmelo Casconea3d811c2017-08-22 01:03:45 +0200106 // In the P4 world 0 is the highest priority, in ONOS the lowest one.
107 // FIXME: move priority conversion to the driver, where different constraints might apply
108 // e.g. less bits for encoding priority in TCAM-based implementations.
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200109 final int newPriority;
Carmelo Casconea3d811c2017-08-22 01:03:45 +0200110 if (rule.priority() > MAX_PI_PRIORITY) {
111 log.warn("Flow rule priority too big, setting translated priority to max value {}: {}",
112 MAX_PI_PRIORITY, rule);
113 newPriority = 0;
114 } else {
115 newPriority = MAX_PI_PRIORITY - rule.priority();
116 }
Carmelo Cascone00a59962017-06-16 17:51:49 +0900117
118 tableEntryBuilder
119 .forTable(piTableId)
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200120 .withPriority(newPriority)
Carmelo Cascone0e896a02017-07-31 07:22:27 +0200121 .withMatchKey(PiMatchKey.builder()
122 .addFieldMatches(fieldMatches)
123 .build())
Yi Tseng82512da2017-08-16 19:46:36 -0700124 .withAction(piTableAction);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900125
126 if (!rule.isPermanent()) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200127 if (tableModel.supportsAging()) {
Carmelo Cascone00a59962017-06-16 17:51:49 +0900128 tableEntryBuilder.withTimeout((double) rule.timeout());
129 } else {
130 log.warn("Flow rule is temporary, but table '{}' doesn't support " +
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200131 "aging, translating to permanent.", tableModel.name());
Carmelo Cascone00a59962017-06-16 17:51:49 +0900132 }
133
134 }
135
136 return tableEntryBuilder.build();
137 }
138
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200139
140 /**
141 * Returns a PI action equivalent to the given treatment, optionally using the given interpreter. This method also
142 * checks that the produced PI table action is suitable for the given table ID and pipeline model. If suitable, the
143 * returned action instance will have parameters well-sized, according to the table model.
144 *
145 * @param treatment traffic treatment
146 * @param interpreter interpreter
147 * @param tableId PI table ID
148 * @param pipelineModel pipeline model
149 * @return PI table action
150 * @throws PiTranslationException if the treatment cannot be translated or if the PI action is not suitable for the
151 * given pipeline model
152 */
153 static PiTableAction translateTreatment(TrafficTreatment treatment, PiPipelineInterpreter interpreter,
154 PiTableId tableId, PiPipelineModel pipelineModel)
155 throws PiTranslationException {
156 PiTableModel tableModel = getTableModel(tableId, pipelineModel);
157 return typeCheckAction(buildAction(treatment, interpreter, tableId), tableModel);
158 }
159
160 private static PiTableModel getTableModel(PiTableId piTableId, PiPipelineModel pipelineModel)
161 throws PiTranslationException {
162 return pipelineModel.table(piTableId.toString())
163 .orElseThrow(() -> new PiTranslationException(format(
164 "Not such a table in pipeline model: %s", piTableId)));
165 }
166
Carmelo Cascone00a59962017-06-16 17:51:49 +0900167 /**
168 * Builds a PI action out of the given treatment, optionally using the given interpreter.
169 */
Yi Tseng82512da2017-08-16 19:46:36 -0700170 private static PiTableAction buildAction(TrafficTreatment treatment, PiPipelineInterpreter interpreter,
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200171 PiTableId tableId)
172 throws PiTranslationException {
Carmelo Cascone00a59962017-06-16 17:51:49 +0900173
174 PiTableAction piTableAction = null;
175
176 // If treatment has only one instruction of type PiInstruction, use that.
177 for (Instruction inst : treatment.allInstructions()) {
178 if (inst.type() == Instruction.Type.PROTOCOL_INDEPENDENT) {
179 if (treatment.allInstructions().size() == 1) {
180 piTableAction = ((PiInstruction) inst).action();
181 } else {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200182 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900183 "Unable to translate treatment, found multiple instructions " +
184 "of which one is protocol-independent: %s", treatment));
185 }
186 }
187 }
188
189 if (piTableAction == null && interpreter != null) {
190 // No PiInstruction, use interpreter to build action.
191 try {
Carmelo Casconef3a1a382017-07-27 12:04:39 -0400192 piTableAction = interpreter.mapTreatment(treatment, tableId);
Carmelo Cascone00a59962017-06-16 17:51:49 +0900193 } catch (PiPipelineInterpreter.PiInterpreterException e) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200194 throw new PiTranslationException(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900195 "Interpreter was unable to translate treatment. " + e.getMessage());
196 }
197 }
198
199 if (piTableAction == null) {
200 // No PiInstruction, no interpreter. It's time to give up.
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200201 throw new PiTranslationException(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900202 "Unable to translate treatment, neither an interpreter or a "
203 + "protocol-independent instruction were provided.");
204 }
205
Yi Tseng82512da2017-08-16 19:46:36 -0700206 return piTableAction;
Carmelo Cascone00a59962017-06-16 17:51:49 +0900207 }
208
Yi Tseng82512da2017-08-16 19:46:36 -0700209 private static PiTableAction typeCheckAction(PiTableAction piTableAction, PiTableModel table)
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200210 throws PiTranslationException {
Yi Tseng82512da2017-08-16 19:46:36 -0700211 switch (piTableAction.type()) {
212 case ACTION:
213 return checkPiAction((PiAction) piTableAction, table);
214 default:
215 // FIXME: should we check? how?
216 return piTableAction;
Carmelo Cascone00a59962017-06-16 17:51:49 +0900217
Yi Tseng82512da2017-08-16 19:46:36 -0700218 }
219 }
220
221 private static PiTableAction checkPiAction(PiAction piAction, PiTableModel table)
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200222 throws PiTranslationException {
Carmelo Cascone00a59962017-06-16 17:51:49 +0900223 // Table supports this action?
224 PiActionModel actionModel = table.action(piAction.id().name()).orElseThrow(
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200225 () -> new PiTranslationException(format("Not such action '%s' for table '%s'",
226 piAction.id(), table.name())));
Carmelo Cascone00a59962017-06-16 17:51:49 +0900227
228 // Is the number of runtime parameters correct?
229 if (actionModel.params().size() != piAction.parameters().size()) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200230 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900231 "Wrong number of runtime parameters for action '%s', expected %d but found %d",
232 actionModel.name(), actionModel.params().size(), piAction.parameters().size()));
233 }
234
235 // Forge a new action instance with well-sized parameters.
236 // The same comment as in typeCheckFieldMatch() about duplicating field match instances applies here.
237 PiAction.Builder newActionBuilder = PiAction.builder().withId(piAction.id());
238 for (PiActionParam param : piAction.parameters()) {
239 PiActionParamModel paramModel = actionModel.param(param.id().name())
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200240 .orElseThrow(() -> new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900241 "Not such parameter '%s' for action '%s'", param.id(), actionModel.name())));
242 try {
243 newActionBuilder.withParameter(new PiActionParam(param.id(),
244 fit(param.value(), paramModel.bitWidth())));
245 } catch (ByteSequenceTrimException e) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200246 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900247 "Size mismatch for parameter '%s' of action '%s': %s",
248 param.id(), piAction.id(), e.getMessage()));
249 }
250 }
251
252 return newActionBuilder.build();
253 }
254
255 /**
256 * Builds a collection of PI field matches out of the given selector, optionally using the given interpreter. The
257 * field matches returned are guaranteed to be compatible for the given table model.
258 */
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200259 private static Collection<PiFieldMatch> translateFieldMatches(PiPipelineInterpreter interpreter,
260 TrafficSelector selector, PiTableModel tableModel)
261 throws PiTranslationException {
Carmelo Cascone00a59962017-06-16 17:51:49 +0900262
263 Map<PiHeaderFieldId, PiFieldMatch> fieldMatches = Maps.newHashMap();
264
265 // If present, find a PiCriterion and get its field matches as a map. Otherwise, use an empty map.
266 Map<PiHeaderFieldId, PiFieldMatch> piCriterionFields = selector.criteria().stream()
267 .filter(c -> c.type().equals(PROTOCOL_INDEPENDENT))
268 .map(c -> (PiCriterion) c)
269 .findFirst()
270 .map(PiCriterion::fieldMatches)
271 .map(c -> {
272 Map<PiHeaderFieldId, PiFieldMatch> fieldMap = Maps.newHashMap();
273 c.forEach(fieldMatch -> fieldMap.put(fieldMatch.fieldId(), fieldMatch));
274 return fieldMap;
275 })
276 .orElse(Maps.newHashMap());
277
278 Set<Criterion> translatedCriteria = Sets.newHashSet();
279 Set<Criterion> ignoredCriteria = Sets.newHashSet();
280 Set<PiHeaderFieldId> usedPiCriterionFields = Sets.newHashSet();
281 Set<PiHeaderFieldId> ignoredPiCriterionFields = Sets.newHashSet();
282
283 for (PiTableMatchFieldModel fieldModel : tableModel.matchFields()) {
284
Carmelo Cascone2cad9ef2017-08-01 21:52:07 +0200285 PiHeaderFieldId fieldId = PiHeaderFieldId.of(fieldModel.field().header().name(),
Carmelo Cascone00a59962017-06-16 17:51:49 +0900286 fieldModel.field().type().name(),
287 fieldModel.field().header().index());
288
289 int bitWidth = fieldModel.field().type().bitWidth();
290 int fieldByteWidth = (int) Math.ceil((double) bitWidth / 8);
291
292 Optional<Criterion.Type> criterionType =
293 interpreter == null
294 ? Optional.empty()
295 : interpreter.mapPiHeaderFieldId(fieldId);
296
297 Criterion criterion = criterionType.map(selector::getCriterion).orElse(null);
298
299 if (!piCriterionFields.containsKey(fieldId) && criterion == null) {
300 // Neither a field in PiCriterion is available nor a Criterion mapping is possible.
301 // Can ignore if the match is ternary or LPM.
302 switch (fieldModel.matchType()) {
303 case TERNARY:
304 // Wildcard the whole field.
305 fieldMatches.put(fieldId, new PiTernaryFieldMatch(
306 fieldId,
307 ImmutableByteSequence.ofZeros(fieldByteWidth),
308 ImmutableByteSequence.ofZeros(fieldByteWidth)));
309 break;
310 case LPM:
311 // LPM with prefix 0
312 fieldMatches.put(fieldId, new PiLpmFieldMatch(fieldId,
313 ImmutableByteSequence.ofZeros(fieldByteWidth),
314 0));
315 break;
316 // FIXME: Can we handle the case of RANGE or VALID match?
317 default:
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200318 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900319 "No value found for required match field '%s'", fieldId));
320 }
321 // Next field.
322 continue;
323 }
324
325 PiFieldMatch fieldMatch = null;
326
327 if (criterion != null) {
328 // Criterion mapping is possible for this field id.
329 try {
330 fieldMatch = translateCriterion(criterion, fieldId, fieldModel.matchType(), bitWidth);
331 translatedCriteria.add(criterion);
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200332 } catch (PiTranslationException ex) {
Carmelo Cascone00a59962017-06-16 17:51:49 +0900333 // Ignore exception if the same field was found in PiCriterion.
334 if (piCriterionFields.containsKey(fieldId)) {
335 ignoredCriteria.add(criterion);
336 } else {
337 throw ex;
338 }
339 }
340 }
341
342 if (piCriterionFields.containsKey(fieldId)) {
343 // Field was found in PiCriterion.
344 if (fieldMatch != null) {
345 // Field was already translated from other criterion.
346 // Throw exception only if we are trying to match on different values of the same field...
347 if (!fieldMatch.equals(piCriterionFields.get(fieldId))) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200348 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900349 "Duplicate match field '%s': instance translated from criterion '%s' is different to " +
350 "what found in PiCriterion.", fieldId, criterion.type()));
351 }
352 ignoredPiCriterionFields.add(fieldId);
353 } else {
354 fieldMatch = piCriterionFields.get(fieldId);
355 fieldMatch = typeCheckFieldMatch(fieldMatch, fieldModel);
356 usedPiCriterionFields.add(fieldId);
357 }
358 }
359
360 fieldMatches.put(fieldId, fieldMatch);
361 }
362
363 // Check if all criteria have been translated.
364 StringJoiner skippedCriteriaJoiner = new StringJoiner(", ");
365 selector.criteria().stream()
366 .filter(c -> !c.type().equals(PROTOCOL_INDEPENDENT))
367 .filter(c -> !translatedCriteria.contains(c) && !ignoredCriteria.contains(c))
368 .forEach(c -> skippedCriteriaJoiner.add(c.type().name()));
369 if (skippedCriteriaJoiner.length() > 0) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200370 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900371 "The following criteria cannot be translated for table '%s': %s",
372 tableModel.name(), skippedCriteriaJoiner.toString()));
373 }
374
375 // Check if all fields found in PiCriterion have been used.
376 StringJoiner skippedPiFieldsJoiner = new StringJoiner(", ");
377 piCriterionFields.keySet().stream()
378 .filter(k -> !usedPiCriterionFields.contains(k) && !ignoredPiCriterionFields.contains(k))
379 .forEach(k -> skippedPiFieldsJoiner.add(k.id()));
380 if (skippedPiFieldsJoiner.length() > 0) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200381 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900382 "The following PiCriterion field matches are not supported in table '%s': %s",
383 tableModel.name(), skippedPiFieldsJoiner.toString()));
384 }
385
386 return fieldMatches.values();
387 }
388
389 private static PiFieldMatch typeCheckFieldMatch(PiFieldMatch fieldMatch, PiTableMatchFieldModel fieldModel)
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200390 throws PiTranslationException {
Carmelo Cascone00a59962017-06-16 17:51:49 +0900391
392 // Check parameter type and size
393 if (!fieldModel.matchType().equals(fieldMatch.type())) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200394 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900395 "Wrong match type for field '%s', expected %s, but found %s",
396 fieldMatch.fieldId(), fieldModel.matchType().name(), fieldMatch.type().name()));
397 }
398
399 int modelBitWidth = fieldModel.field().type().bitWidth();
400
401 /*
402 Here we try to be robust against wrong size fields with the goal of having PiCriterion independent of the
403 pipeline model. We duplicate the field match, fitting the byte sequences to the bit-width specified in the
404 model. This operation is expensive when performed for each field match of each flow rule, but should be
405 mitigated by the translation cache provided by PiFlowRuleTranslationServiceImpl.
406 */
407
408 try {
409 switch (fieldModel.matchType()) {
410 case EXACT:
411 return new PiExactFieldMatch(fieldMatch.fieldId(),
412 fit(((PiExactFieldMatch) fieldMatch).value(), modelBitWidth));
413 case TERNARY:
414 return new PiTernaryFieldMatch(fieldMatch.fieldId(),
415 fit(((PiTernaryFieldMatch) fieldMatch).value(), modelBitWidth),
416 fit(((PiTernaryFieldMatch) fieldMatch).mask(), modelBitWidth));
417 case LPM:
418 PiLpmFieldMatch lpmfield = (PiLpmFieldMatch) fieldMatch;
419 if (lpmfield.prefixLength() > modelBitWidth) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200420 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900421 "Invalid prefix length for LPM field '%s', found %d but field has bit-width %d",
422 fieldMatch.fieldId(), lpmfield.prefixLength(), modelBitWidth));
423 }
424 return new PiLpmFieldMatch(fieldMatch.fieldId(),
425 fit(lpmfield.value(), modelBitWidth),
426 lpmfield.prefixLength());
427 case RANGE:
428 return new PiRangeFieldMatch(fieldMatch.fieldId(),
429 fit(((PiRangeFieldMatch) fieldMatch).lowValue(), modelBitWidth),
430 fit(((PiRangeFieldMatch) fieldMatch).highValue(), modelBitWidth));
431 case VALID:
432 return fieldMatch;
433 default:
434 // Should never be here.
435 throw new RuntimeException(
436 "Unrecognized match type " + fieldModel.matchType().name());
437 }
438 } catch (ByteSequenceTrimException e) {
Carmelo Cascone87b9b392017-10-02 18:33:20 +0200439 throw new PiTranslationException(format(
Carmelo Cascone00a59962017-06-16 17:51:49 +0900440 "Size mismatch for field %s: %s", fieldMatch.fieldId(), e.getMessage()));
441 }
442 }
443}