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