xref: /unit/src/java/nginx/unit/websocket/Util.java (revision 2617:18a10bb7346d)
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package nginx.unit.websocket;
18 
19 import java.io.InputStream;
20 import java.io.Reader;
21 import java.lang.reflect.GenericArrayType;
22 import java.lang.reflect.Method;
23 import java.lang.reflect.ParameterizedType;
24 import java.lang.reflect.Type;
25 import java.lang.reflect.TypeVariable;
26 import java.nio.ByteBuffer;
27 import java.security.NoSuchAlgorithmException;
28 import java.security.SecureRandom;
29 import java.util.ArrayList;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Queue;
33 import java.util.Set;
34 import java.util.concurrent.ConcurrentLinkedQueue;
35 
36 import javax.websocket.CloseReason.CloseCode;
37 import javax.websocket.CloseReason.CloseCodes;
38 import javax.websocket.Decoder;
39 import javax.websocket.Decoder.Binary;
40 import javax.websocket.Decoder.BinaryStream;
41 import javax.websocket.Decoder.Text;
42 import javax.websocket.Decoder.TextStream;
43 import javax.websocket.DeploymentException;
44 import javax.websocket.Encoder;
45 import javax.websocket.EndpointConfig;
46 import javax.websocket.Extension;
47 import javax.websocket.MessageHandler;
48 import javax.websocket.PongMessage;
49 import javax.websocket.Session;
50 
51 import org.apache.tomcat.util.res.StringManager;
52 import nginx.unit.websocket.pojo.PojoMessageHandlerPartialBinary;
53 import nginx.unit.websocket.pojo.PojoMessageHandlerWholeBinary;
54 import nginx.unit.websocket.pojo.PojoMessageHandlerWholeText;
55 
56 /**
57  * Utility class for internal use only within the
58  * {@link nginx.unit.websocket} package.
59  */
60 public class Util {
61 
62     private static final StringManager sm = StringManager.getManager(Util.class);
63     private static final Queue<SecureRandom> randoms =
64             new ConcurrentLinkedQueue<>();
65 
Util()66     private Util() {
67         // Hide default constructor
68     }
69 
70 
isControl(byte opCode)71     static boolean isControl(byte opCode) {
72         return (opCode & 0x08) != 0;
73     }
74 
75 
isText(byte opCode)76     static boolean isText(byte opCode) {
77         return opCode == Constants.OPCODE_TEXT;
78     }
79 
80 
isContinuation(byte opCode)81     static boolean isContinuation(byte opCode) {
82         return opCode == Constants.OPCODE_CONTINUATION;
83     }
84 
85 
getCloseCode(int code)86     static CloseCode getCloseCode(int code) {
87         if (code > 2999 && code < 5000) {
88             return CloseCodes.getCloseCode(code);
89         }
90         switch (code) {
91             case 1000:
92                 return CloseCodes.NORMAL_CLOSURE;
93             case 1001:
94                 return CloseCodes.GOING_AWAY;
95             case 1002:
96                 return CloseCodes.PROTOCOL_ERROR;
97             case 1003:
98                 return CloseCodes.CANNOT_ACCEPT;
99             case 1004:
100                 // Should not be used in a close frame
101                 // return CloseCodes.RESERVED;
102                 return CloseCodes.PROTOCOL_ERROR;
103             case 1005:
104                 // Should not be used in a close frame
105                 // return CloseCodes.NO_STATUS_CODE;
106                 return CloseCodes.PROTOCOL_ERROR;
107             case 1006:
108                 // Should not be used in a close frame
109                 // return CloseCodes.CLOSED_ABNORMALLY;
110                 return CloseCodes.PROTOCOL_ERROR;
111             case 1007:
112                 return CloseCodes.NOT_CONSISTENT;
113             case 1008:
114                 return CloseCodes.VIOLATED_POLICY;
115             case 1009:
116                 return CloseCodes.TOO_BIG;
117             case 1010:
118                 return CloseCodes.NO_EXTENSION;
119             case 1011:
120                 return CloseCodes.UNEXPECTED_CONDITION;
121             case 1012:
122                 // Not in RFC6455
123                 // return CloseCodes.SERVICE_RESTART;
124                 return CloseCodes.PROTOCOL_ERROR;
125             case 1013:
126                 // Not in RFC6455
127                 // return CloseCodes.TRY_AGAIN_LATER;
128                 return CloseCodes.PROTOCOL_ERROR;
129             case 1015:
130                 // Should not be used in a close frame
131                 // return CloseCodes.TLS_HANDSHAKE_FAILURE;
132                 return CloseCodes.PROTOCOL_ERROR;
133             default:
134                 return CloseCodes.PROTOCOL_ERROR;
135         }
136     }
137 
138 
generateMask()139     static byte[] generateMask() {
140         // SecureRandom is not thread-safe so need to make sure only one thread
141         // uses it at a time. In theory, the pool could grow to the same size
142         // as the number of request processing threads. In reality it will be
143         // a lot smaller.
144 
145         // Get a SecureRandom from the pool
146         SecureRandom sr = randoms.poll();
147 
148         // If one isn't available, generate a new one
149         if (sr == null) {
150             try {
151                 sr = SecureRandom.getInstance("SHA1PRNG");
152             } catch (NoSuchAlgorithmException e) {
153                 // Fall back to platform default
154                 sr = new SecureRandom();
155             }
156         }
157 
158         // Generate the mask
159         byte[] result = new byte[4];
160         sr.nextBytes(result);
161 
162         // Put the SecureRandom back in the poll
163         randoms.add(sr);
164 
165         return result;
166     }
167 
168 
getMessageType(MessageHandler listener)169     static Class<?> getMessageType(MessageHandler listener) {
170         return Util.getGenericType(MessageHandler.class,
171                 listener.getClass()).getClazz();
172     }
173 
174 
getDecoderType(Class<? extends Decoder> decoder)175     private static Class<?> getDecoderType(Class<? extends Decoder> decoder) {
176         return Util.getGenericType(Decoder.class, decoder).getClazz();
177     }
178 
179 
getEncoderType(Class<? extends Encoder> encoder)180     static Class<?> getEncoderType(Class<? extends Encoder> encoder) {
181         return Util.getGenericType(Encoder.class, encoder).getClazz();
182     }
183 
184 
getGenericType(Class<T> type, Class<? extends T> clazz)185     private static <T> TypeResult getGenericType(Class<T> type,
186             Class<? extends T> clazz) {
187 
188         // Look to see if this class implements the interface of interest
189 
190         // Get all the interfaces
191         Type[] interfaces = clazz.getGenericInterfaces();
192         for (Type iface : interfaces) {
193             // Only need to check interfaces that use generics
194             if (iface instanceof ParameterizedType) {
195                 ParameterizedType pi = (ParameterizedType) iface;
196                 // Look for the interface of interest
197                 if (pi.getRawType() instanceof Class) {
198                     if (type.isAssignableFrom((Class<?>) pi.getRawType())) {
199                         return getTypeParameter(
200                                 clazz, pi.getActualTypeArguments()[0]);
201                     }
202                 }
203             }
204         }
205 
206         // Interface not found on this class. Look at the superclass.
207         @SuppressWarnings("unchecked")
208         Class<? extends T> superClazz =
209                 (Class<? extends T>) clazz.getSuperclass();
210         if (superClazz == null) {
211             // Finished looking up the class hierarchy without finding anything
212             return null;
213         }
214 
215         TypeResult superClassTypeResult = getGenericType(type, superClazz);
216         int dimension = superClassTypeResult.getDimension();
217         if (superClassTypeResult.getIndex() == -1 && dimension == 0) {
218             // Superclass implements interface and defines explicit type for
219             // the interface of interest
220             return superClassTypeResult;
221         }
222 
223         if (superClassTypeResult.getIndex() > -1) {
224             // Superclass implements interface and defines unknown type for
225             // the interface of interest
226             // Map that unknown type to the generic types defined in this class
227             ParameterizedType superClassType =
228                     (ParameterizedType) clazz.getGenericSuperclass();
229             TypeResult result = getTypeParameter(clazz,
230                     superClassType.getActualTypeArguments()[
231                             superClassTypeResult.getIndex()]);
232             result.incrementDimension(superClassTypeResult.getDimension());
233             if (result.getClazz() != null && result.getDimension() > 0) {
234                 superClassTypeResult = result;
235             } else {
236                 return result;
237             }
238         }
239 
240         if (superClassTypeResult.getDimension() > 0) {
241             StringBuilder className = new StringBuilder();
242             for (int i = 0; i < dimension; i++) {
243                 className.append('[');
244             }
245             className.append('L');
246             className.append(superClassTypeResult.getClazz().getCanonicalName());
247             className.append(';');
248 
249             Class<?> arrayClazz;
250             try {
251                 arrayClazz = Class.forName(className.toString());
252             } catch (ClassNotFoundException e) {
253                 throw new IllegalArgumentException(e);
254             }
255 
256             return new TypeResult(arrayClazz, -1, 0);
257         }
258 
259         // Error will be logged further up the call stack
260         return null;
261     }
262 
263 
264     /*
265      * For a generic parameter, return either the Class used or if the type
266      * is unknown, the index for the type in definition of the class
267      */
getTypeParameter(Class<?> clazz, Type argType)268     private static TypeResult getTypeParameter(Class<?> clazz, Type argType) {
269         if (argType instanceof Class<?>) {
270             return new TypeResult((Class<?>) argType, -1, 0);
271         } else if (argType instanceof ParameterizedType) {
272             return new TypeResult((Class<?>)((ParameterizedType) argType).getRawType(), -1, 0);
273         } else if (argType instanceof GenericArrayType) {
274             Type arrayElementType = ((GenericArrayType) argType).getGenericComponentType();
275             TypeResult result = getTypeParameter(clazz, arrayElementType);
276             result.incrementDimension(1);
277             return result;
278         } else {
279             TypeVariable<?>[] tvs = clazz.getTypeParameters();
280             for (int i = 0; i < tvs.length; i++) {
281                 if (tvs[i].equals(argType)) {
282                     return new TypeResult(null, i, 0);
283                 }
284             }
285             return null;
286         }
287     }
288 
289 
isPrimitive(Class<?> clazz)290     public static boolean isPrimitive(Class<?> clazz) {
291         if (clazz.isPrimitive()) {
292             return true;
293         } else if(clazz.equals(Boolean.class) ||
294                 clazz.equals(Byte.class) ||
295                 clazz.equals(Character.class) ||
296                 clazz.equals(Double.class) ||
297                 clazz.equals(Float.class) ||
298                 clazz.equals(Integer.class) ||
299                 clazz.equals(Long.class) ||
300                 clazz.equals(Short.class)) {
301             return true;
302         }
303         return false;
304     }
305 
306 
coerceToType(Class<?> type, String value)307     public static Object coerceToType(Class<?> type, String value) {
308         if (type.equals(String.class)) {
309             return value;
310         } else if (type.equals(boolean.class) || type.equals(Boolean.class)) {
311             return Boolean.valueOf(value);
312         } else if (type.equals(byte.class) || type.equals(Byte.class)) {
313             return Byte.valueOf(value);
314         } else if (type.equals(char.class) || type.equals(Character.class)) {
315             return Character.valueOf(value.charAt(0));
316         } else if (type.equals(double.class) || type.equals(Double.class)) {
317             return Double.valueOf(value);
318         } else if (type.equals(float.class) || type.equals(Float.class)) {
319             return Float.valueOf(value);
320         } else if (type.equals(int.class) || type.equals(Integer.class)) {
321             return Integer.valueOf(value);
322         } else if (type.equals(long.class) || type.equals(Long.class)) {
323             return Long.valueOf(value);
324         } else if (type.equals(short.class) || type.equals(Short.class)) {
325             return Short.valueOf(value);
326         } else {
327             throw new IllegalArgumentException(sm.getString(
328                     "util.invalidType", value, type.getName()));
329         }
330     }
331 
332 
getDecoders( List<Class<? extends Decoder>> decoderClazzes)333     public static List<DecoderEntry> getDecoders(
334             List<Class<? extends Decoder>> decoderClazzes)
335                     throws DeploymentException {
336 
337         List<DecoderEntry> result = new ArrayList<>();
338         if (decoderClazzes != null) {
339             for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
340                 // Need to instantiate decoder to ensure it is valid and that
341                 // deployment can be failed if it is not
342                 @SuppressWarnings("unused")
343                 Decoder instance;
344                 try {
345                     instance = decoderClazz.getConstructor().newInstance();
346                 } catch (ReflectiveOperationException e) {
347                     throw new DeploymentException(
348                             sm.getString("pojoMethodMapping.invalidDecoder",
349                                     decoderClazz.getName()), e);
350                 }
351                 DecoderEntry entry = new DecoderEntry(
352                         Util.getDecoderType(decoderClazz), decoderClazz);
353                 result.add(entry);
354             }
355         }
356 
357         return result;
358     }
359 
360 
getMessageHandlers(Class<?> target, MessageHandler listener, EndpointConfig endpointConfig, Session session)361     static Set<MessageHandlerResult> getMessageHandlers(Class<?> target,
362             MessageHandler listener, EndpointConfig endpointConfig,
363             Session session) {
364 
365         // Will never be more than 2 types
366         Set<MessageHandlerResult> results = new HashSet<>(2);
367 
368         // Simple cases - handlers already accepts one of the types expected by
369         // the frame handling code
370         if (String.class.isAssignableFrom(target)) {
371             MessageHandlerResult result =
372                     new MessageHandlerResult(listener,
373                             MessageHandlerResultType.TEXT);
374             results.add(result);
375         } else if (ByteBuffer.class.isAssignableFrom(target)) {
376             MessageHandlerResult result =
377                     new MessageHandlerResult(listener,
378                             MessageHandlerResultType.BINARY);
379             results.add(result);
380         } else if (PongMessage.class.isAssignableFrom(target)) {
381             MessageHandlerResult result =
382                     new MessageHandlerResult(listener,
383                             MessageHandlerResultType.PONG);
384             results.add(result);
385         // Handler needs wrapping and optional decoder to convert it to one of
386         // the types expected by the frame handling code
387         } else if (byte[].class.isAssignableFrom(target)) {
388             boolean whole = MessageHandler.Whole.class.isAssignableFrom(listener.getClass());
389             MessageHandlerResult result = new MessageHandlerResult(
390                     whole ? new PojoMessageHandlerWholeBinary(listener,
391                                     getOnMessageMethod(listener), session,
392                                     endpointConfig, matchDecoders(target, endpointConfig, true),
393                                     new Object[1], 0, true, -1, false, -1) :
394                             new PojoMessageHandlerPartialBinary(listener,
395                                     getOnMessagePartialMethod(listener), session,
396                                     new Object[2], 0, true, 1, -1, -1),
397                     MessageHandlerResultType.BINARY);
398             results.add(result);
399         } else if (InputStream.class.isAssignableFrom(target)) {
400             MessageHandlerResult result = new MessageHandlerResult(
401                     new PojoMessageHandlerWholeBinary(listener,
402                             getOnMessageMethod(listener), session,
403                             endpointConfig, matchDecoders(target, endpointConfig, true),
404                             new Object[1], 0, true, -1, true, -1),
405                     MessageHandlerResultType.BINARY);
406             results.add(result);
407         } else if (Reader.class.isAssignableFrom(target)) {
408             MessageHandlerResult result = new MessageHandlerResult(
409                     new PojoMessageHandlerWholeText(listener,
410                             getOnMessageMethod(listener), session,
411                             endpointConfig, matchDecoders(target, endpointConfig, false),
412                             new Object[1], 0, true, -1, -1),
413                     MessageHandlerResultType.TEXT);
414             results.add(result);
415         } else {
416             // Handler needs wrapping and requires decoder to convert it to one
417             // of the types expected by the frame handling code
418             DecoderMatch decoderMatch = matchDecoders(target, endpointConfig);
419             Method m = getOnMessageMethod(listener);
420             if (decoderMatch.getBinaryDecoders().size() > 0) {
421                 MessageHandlerResult result = new MessageHandlerResult(
422                         new PojoMessageHandlerWholeBinary(listener, m, session,
423                                 endpointConfig,
424                                 decoderMatch.getBinaryDecoders(), new Object[1],
425                                 0, false, -1, false, -1),
426                                 MessageHandlerResultType.BINARY);
427                 results.add(result);
428             }
429             if (decoderMatch.getTextDecoders().size() > 0) {
430                 MessageHandlerResult result = new MessageHandlerResult(
431                         new PojoMessageHandlerWholeText(listener, m, session,
432                                 endpointConfig,
433                                 decoderMatch.getTextDecoders(), new Object[1],
434                                 0, false, -1, -1),
435                                 MessageHandlerResultType.TEXT);
436                 results.add(result);
437             }
438         }
439 
440         if (results.size() == 0) {
441             throw new IllegalArgumentException(
442                     sm.getString("wsSession.unknownHandler", listener, target));
443         }
444 
445         return results;
446     }
447 
matchDecoders(Class<?> target, EndpointConfig endpointConfig, boolean binary)448     private static List<Class<? extends Decoder>> matchDecoders(Class<?> target,
449             EndpointConfig endpointConfig, boolean binary) {
450         DecoderMatch decoderMatch = matchDecoders(target, endpointConfig);
451         if (binary) {
452             if (decoderMatch.getBinaryDecoders().size() > 0) {
453                 return decoderMatch.getBinaryDecoders();
454             }
455         } else if (decoderMatch.getTextDecoders().size() > 0) {
456             return decoderMatch.getTextDecoders();
457         }
458         return null;
459     }
460 
matchDecoders(Class<?> target, EndpointConfig endpointConfig)461     private static DecoderMatch matchDecoders(Class<?> target,
462             EndpointConfig endpointConfig) {
463         DecoderMatch decoderMatch;
464         try {
465             List<Class<? extends Decoder>> decoders =
466                     endpointConfig.getDecoders();
467             List<DecoderEntry> decoderEntries = getDecoders(decoders);
468             decoderMatch = new DecoderMatch(target, decoderEntries);
469         } catch (DeploymentException e) {
470             throw new IllegalArgumentException(e);
471         }
472         return decoderMatch;
473     }
474 
parseExtensionHeader(List<Extension> extensions, String header)475     public static void parseExtensionHeader(List<Extension> extensions,
476             String header) {
477         // The relevant ABNF for the Sec-WebSocket-Extensions is as follows:
478         //      extension-list = 1#extension
479         //      extension = extension-token *( ";" extension-param )
480         //      extension-token = registered-token
481         //      registered-token = token
482         //      extension-param = token [ "=" (token | quoted-string) ]
483         //             ; When using the quoted-string syntax variant, the value
484         //             ; after quoted-string unescaping MUST conform to the
485         //             ; 'token' ABNF.
486         //
487         // The limiting of parameter values to tokens or "quoted tokens" makes
488         // the parsing of the header significantly simpler and allows a number
489         // of short-cuts to be taken.
490 
491         // Step one, split the header into individual extensions using ',' as a
492         // separator
493         String unparsedExtensions[] = header.split(",");
494         for (String unparsedExtension : unparsedExtensions) {
495             // Step two, split the extension into the registered name and
496             // parameter/value pairs using ';' as a separator
497             String unparsedParameters[] = unparsedExtension.split(";");
498             WsExtension extension = new WsExtension(unparsedParameters[0].trim());
499 
500             for (int i = 1; i < unparsedParameters.length; i++) {
501                 int equalsPos = unparsedParameters[i].indexOf('=');
502                 String name;
503                 String value;
504                 if (equalsPos == -1) {
505                     name = unparsedParameters[i].trim();
506                     value = null;
507                 } else {
508                     name = unparsedParameters[i].substring(0, equalsPos).trim();
509                     value = unparsedParameters[i].substring(equalsPos + 1).trim();
510                     int len = value.length();
511                     if (len > 1) {
512                         if (value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') {
513                             value = value.substring(1, value.length() - 1);
514                         }
515                     }
516                 }
517                 // Make sure value doesn't contain any of the delimiters since
518                 // that would indicate something went wrong
519                 if (containsDelims(name) || containsDelims(value)) {
520                     throw new IllegalArgumentException(sm.getString(
521                             "util.notToken", name, value));
522                 }
523                 if (value != null &&
524                         (value.indexOf(',') > -1 || value.indexOf(';') > -1 ||
525                         value.indexOf('\"') > -1 || value.indexOf('=') > -1)) {
526                     throw new IllegalArgumentException(sm.getString("", value));
527                 }
528                 extension.addParameter(new WsExtensionParameter(name, value));
529             }
530             extensions.add(extension);
531         }
532     }
533 
534 
containsDelims(String input)535     private static boolean containsDelims(String input) {
536         if (input == null || input.length() == 0) {
537             return false;
538         }
539         for (char c : input.toCharArray()) {
540             switch (c) {
541                 case ',':
542                 case ';':
543                 case '\"':
544                 case '=':
545                     return true;
546                 default:
547                     // NO_OP
548             }
549 
550         }
551         return false;
552     }
553 
getOnMessageMethod(MessageHandler listener)554     private static Method getOnMessageMethod(MessageHandler listener) {
555         try {
556             return listener.getClass().getMethod("onMessage", Object.class);
557         } catch (NoSuchMethodException | SecurityException e) {
558             throw new IllegalArgumentException(
559                     sm.getString("util.invalidMessageHandler"), e);
560         }
561     }
562 
getOnMessagePartialMethod(MessageHandler listener)563     private static Method getOnMessagePartialMethod(MessageHandler listener) {
564         try {
565             return listener.getClass().getMethod("onMessage", Object.class, Boolean.TYPE);
566         } catch (NoSuchMethodException | SecurityException e) {
567             throw new IllegalArgumentException(
568                     sm.getString("util.invalidMessageHandler"), e);
569         }
570     }
571 
572 
573     public static class DecoderMatch {
574 
575         private final List<Class<? extends Decoder>> textDecoders =
576                 new ArrayList<>();
577         private final List<Class<? extends Decoder>> binaryDecoders =
578                 new ArrayList<>();
579         private final Class<?> target;
580 
DecoderMatch(Class<?> target, List<DecoderEntry> decoderEntries)581         public DecoderMatch(Class<?> target, List<DecoderEntry> decoderEntries) {
582             this.target = target;
583             for (DecoderEntry decoderEntry : decoderEntries) {
584                 if (decoderEntry.getClazz().isAssignableFrom(target)) {
585                     if (Binary.class.isAssignableFrom(
586                             decoderEntry.getDecoderClazz())) {
587                         binaryDecoders.add(decoderEntry.getDecoderClazz());
588                         // willDecode() method means this decoder may or may not
589                         // decode a message so need to carry on checking for
590                         // other matches
591                     } else if (BinaryStream.class.isAssignableFrom(
592                             decoderEntry.getDecoderClazz())) {
593                         binaryDecoders.add(decoderEntry.getDecoderClazz());
594                         // Stream decoders have to process the message so no
595                         // more decoders can be matched
596                         break;
597                     } else if (Text.class.isAssignableFrom(
598                             decoderEntry.getDecoderClazz())) {
599                         textDecoders.add(decoderEntry.getDecoderClazz());
600                         // willDecode() method means this decoder may or may not
601                         // decode a message so need to carry on checking for
602                         // other matches
603                     } else if (TextStream.class.isAssignableFrom(
604                             decoderEntry.getDecoderClazz())) {
605                         textDecoders.add(decoderEntry.getDecoderClazz());
606                         // Stream decoders have to process the message so no
607                         // more decoders can be matched
608                         break;
609                     } else {
610                         throw new IllegalArgumentException(
611                                 sm.getString("util.unknownDecoderType"));
612                     }
613                 }
614             }
615         }
616 
617 
getTextDecoders()618         public List<Class<? extends Decoder>> getTextDecoders() {
619             return textDecoders;
620         }
621 
622 
getBinaryDecoders()623         public List<Class<? extends Decoder>> getBinaryDecoders() {
624             return binaryDecoders;
625         }
626 
627 
getTarget()628         public Class<?> getTarget() {
629             return target;
630         }
631 
632 
hasMatches()633         public boolean hasMatches() {
634             return (textDecoders.size() > 0) || (binaryDecoders.size() > 0);
635         }
636     }
637 
638 
639     private static class TypeResult {
640         private final Class<?> clazz;
641         private final int index;
642         private int dimension;
643 
TypeResult(Class<?> clazz, int index, int dimension)644         public TypeResult(Class<?> clazz, int index, int dimension) {
645             this.clazz= clazz;
646             this.index = index;
647             this.dimension = dimension;
648         }
649 
getClazz()650         public Class<?> getClazz() {
651             return clazz;
652         }
653 
getIndex()654         public int getIndex() {
655             return index;
656         }
657 
getDimension()658         public int getDimension() {
659             return dimension;
660         }
661 
incrementDimension(int inc)662         public void incrementDimension(int inc) {
663             dimension += inc;
664         }
665     }
666 }
667