View Javadoc
1 /* 2 * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.16.2.2 2004/01/06 22:09:04 olegk Exp $ 3 * $Revision: 1.16.2.2 $ 4 * $Date: 2004/01/06 22:09:04 $ 5 * 6 * ==================================================================== 7 * 8 * The Apache Software License, Version 1.1 9 * 10 * Copyright (c) 2002-2003 The Apache Software Foundation. All rights 11 * reserved. 12 * 13 * Redistribution and use in source and binary forms, with or without 14 * modification, are permitted provided that the following conditions 15 * are met: 16 * 17 * 1. Redistributions of source code must retain the above copyright 18 * notice, this list of conditions and the following disclaimer. 19 * 20 * 2. Redistributions in binary form must reproduce the above copyright 21 * notice, this list of conditions and the following disclaimer in 22 * the documentation and/or other materials provided with the 23 * distribution. 24 * 25 * 3. The end-user documentation included with the redistribution, if 26 * any, must include the following acknowlegement: 27 * "This product includes software developed by the 28 * Apache Software Foundation (http://www.apache.org/)." 29 * Alternately, this acknowlegement may appear in the software itself, 30 * if and wherever such third-party acknowlegements normally appear. 31 * 32 * 4. The names "The Jakarta Project", "Commons", and "Apache Software 33 * Foundation" must not be used to endorse or promote products derived 34 * from this software without prior written permission. For written 35 * permission, please contact apache@apache.org. 36 * 37 * 5. Products derived from this software may not be called "Apache" 38 * nor may "Apache" appear in their names without prior written 39 * permission of the Apache Group. 40 * 41 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED 42 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 43 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR 45 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 46 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 47 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 48 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 49 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 50 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 51 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 52 * SUCH DAMAGE. 53 * ==================================================================== 54 * 55 * This software consists of voluntary contributions made by many 56 * individuals on behalf of the Apache Software Foundation. For more 57 * information on the Apache Software Foundation, please see 58 * <http://www.apache.org/>. 59 * 60 * [Additional notices, if required by prior licensing conditions] 61 * 62 */ 63 64 package org.apache.commons.httpclient.cookie; 65 66 import java.util.Date; 67 import java.util.LinkedList; 68 import java.util.List; 69 70 import org.apache.commons.httpclient.Cookie; 71 import org.apache.commons.httpclient.Header; 72 import org.apache.commons.httpclient.HeaderElement; 73 import org.apache.commons.httpclient.HttpException; 74 import org.apache.commons.httpclient.NameValuePair; 75 import org.apache.commons.httpclient.util.DateParseException; 76 import org.apache.commons.httpclient.util.DateParser; 77 import org.apache.commons.logging.Log; 78 import org.apache.commons.logging.LogFactory; 79 80 /*** 81 * 82 * Cookie management functions shared by all specification. 83 * 84 * @author B.C. Holmes 85 * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a> 86 * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a> 87 * @author Rod Waldhoff 88 * @author dIon Gillard 89 * @author Sean C. Sullivan 90 * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a> 91 * @author Marc A. Saegesser 92 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> 93 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> 94 * 95 * @since 2.0 96 */ 97 public class CookieSpecBase implements CookieSpec { 98 99 /*** Log object */ 100 protected static final Log LOG = LogFactory.getLog(CookieSpec.class); 101 102 /*** Default constructor */ 103 public CookieSpecBase() { 104 super(); 105 } 106 107 108 /*** 109 * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s. 110 * 111 * <P>The syntax for the Set-Cookie response header is: 112 * 113 * <PRE> 114 * set-cookie = "Set-Cookie:" cookies 115 * cookies = 1#cookie 116 * cookie = NAME "=" VALUE * (";" cookie-av) 117 * NAME = attr 118 * VALUE = value 119 * cookie-av = "Comment" "=" value 120 * | "Domain" "=" value 121 * | "Max-Age" "=" value 122 * | "Path" "=" value 123 * | "Secure" 124 * | "Version" "=" 1*DIGIT 125 * </PRE> 126 * 127 * @param host the host from which the <tt>Set-Cookie</tt> value was 128 * received 129 * @param port the port from which the <tt>Set-Cookie</tt> value was 130 * received 131 * @param path the path from which the <tt>Set-Cookie</tt> value was 132 * received 133 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> value was 134 * received over secure conection 135 * @param header the <tt>Set-Cookie</tt> received from the server 136 * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value 137 * @throws MalformedCookieException if an exception occurs during parsing 138 */ 139 public Cookie[] parse(String host, int port, String path, 140 boolean secure, final String header) 141 throws MalformedCookieException { 142 143 LOG.trace("enter CookieSpecBase.parse(" 144 + "String, port, path, boolean, Header)"); 145 146 if (host == null) { 147 throw new IllegalArgumentException( 148 "Host of origin may not be null"); 149 } 150 if (host.trim().equals("")) { 151 throw new IllegalArgumentException( 152 "Host of origin may not be blank"); 153 } 154 if (port < 0) { 155 throw new IllegalArgumentException("Invalid port: " + port); 156 } 157 if (path == null) { 158 throw new IllegalArgumentException( 159 "Path of origin may not be null."); 160 } 161 if (header == null) { 162 throw new IllegalArgumentException("Header may not be null."); 163 } 164 165 if (path.trim().equals("")) { 166 path = PATH_DELIM; 167 } 168 host = host.toLowerCase(); 169 170 HeaderElement[] headerElements = null; 171 try { 172 headerElements = HeaderElement.parse(header); 173 } catch (HttpException e) { 174 throw new MalformedCookieException(e.getMessage()); 175 } 176 177 String defaultPath = path; 178 int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM); 179 if (lastSlashIndex >= 0) { 180 if (lastSlashIndex == 0) { 181 //Do not remove the very first slash 182 lastSlashIndex = 1; 183 } 184 defaultPath = defaultPath.substring(0, lastSlashIndex); 185 } 186 187 Cookie[] cookies = new Cookie[headerElements.length]; 188 189 for (int i = 0; i < headerElements.length; i++) { 190 191 HeaderElement headerelement = headerElements[i]; 192 Cookie cookie = null; 193 try { 194 cookie = new Cookie(host, 195 headerelement.getName(), 196 headerelement.getValue(), 197 defaultPath, 198 null, 199 false); 200 } catch (IllegalArgumentException e) { 201 throw new MalformedCookieException(e.getMessage()); 202 } 203 // cycle through the parameters 204 NameValuePair[] parameters = headerelement.getParameters(); 205 // could be null. In case only a header element and no parameters. 206 if (parameters != null) { 207 208 for (int j = 0; j < parameters.length; j++) { 209 parseAttribute(parameters[j], cookie); 210 } 211 } 212 cookies[i] = cookie; 213 } 214 return cookies; 215 } 216 217 218 /*** 219 * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link 220 * Cookie}s. 221 * 222 * <P>The syntax for the Set-Cookie response header is: 223 * 224 * <PRE> 225 * set-cookie = "Set-Cookie:" cookies 226 * cookies = 1#cookie 227 * cookie = NAME "=" VALUE * (";" cookie-av) 228 * NAME = attr 229 * VALUE = value 230 * cookie-av = "Comment" "=" value 231 * | "Domain" "=" value 232 * | "Max-Age" "=" value 233 * | "Path" "=" value 234 * | "Secure" 235 * | "Version" "=" 1*DIGIT 236 * </PRE> 237 * 238 * @param host the host from which the <tt>Set-Cookie</tt> header was 239 * received 240 * @param port the port from which the <tt>Set-Cookie</tt> header was 241 * received 242 * @param path the path from which the <tt>Set-Cookie</tt> header was 243 * received 244 * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was 245 * received over secure conection 246 * @param header the <tt>Set-Cookie</tt> received from the server 247 * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie" 248 * </tt> header 249 * @throws MalformedCookieException if an exception occurs during parsing 250 */ 251 public Cookie[] parse( 252 String host, int port, String path, boolean secure, final Header header) 253 throws MalformedCookieException { 254 255 LOG.trace("enter CookieSpecBase.parse(" 256 + "String, port, path, boolean, String)"); 257 if (header == null) { 258 throw new IllegalArgumentException("Header may not be null."); 259 } 260 return parse(host, port, path, secure, header.getValue()); 261 } 262 263 264 /*** 265 * Parse the cookie attribute and update the corresponsing {@link Cookie} 266 * properties. 267 * 268 * @param attribute {@link HeaderElement} cookie attribute from the 269 * <tt>Set- Cookie</tt> 270 * @param cookie {@link Cookie} to be updated 271 * @throws MalformedCookieException if an exception occurs during parsing 272 */ 273 274 public void parseAttribute( 275 final NameValuePair attribute, final Cookie cookie) 276 throws MalformedCookieException { 277 278 if (attribute == null) { 279 throw new IllegalArgumentException("Attribute may not be null."); 280 } 281 if (cookie == null) { 282 throw new IllegalArgumentException("Cookie may not be null."); 283 } 284 final String paramName = attribute.getName().toLowerCase(); 285 String paramValue = attribute.getValue(); 286 287 if (paramName.equals("path")) { 288 289 if ((paramValue == null) || (paramValue.trim().equals(""))) { 290 paramValue = "/"; 291 } 292 cookie.setPath(paramValue); 293 cookie.setPathAttributeSpecified(true); 294 295 } else if (paramName.equals("domain")) { 296 297 if (paramValue == null) { 298 throw new MalformedCookieException( 299 "Missing value for domain attribute"); 300 } 301 if (paramValue.trim().equals("")) { 302 throw new MalformedCookieException( 303 "Blank value for domain attribute"); 304 } 305 cookie.setDomain(paramValue); 306 cookie.setDomainAttributeSpecified(true); 307 308 } else if (paramName.equals("max-age")) { 309 310 if (paramValue == null) { 311 throw new MalformedCookieException( 312 "Missing value for max-age attribute"); 313 } 314 int age; 315 try { 316 age = Integer.parseInt(paramValue); 317 } catch (NumberFormatException e) { 318 throw new MalformedCookieException ("Invalid max-age " 319 + "attribute: " + e.getMessage()); 320 } 321 cookie.setExpiryDate( 322 new Date(System.currentTimeMillis() + age * 1000L)); 323 324 } else if (paramName.equals("secure")) { 325 326 cookie.setSecure(true); 327 328 } else if (paramName.equals("comment")) { 329 330 cookie.setComment(paramValue); 331 332 } else if (paramName.equals("expires")) { 333 334 if (paramValue == null) { 335 throw new MalformedCookieException( 336 "Missing value for expires attribute"); 337 } 338 // trim single quotes around expiry if present 339 // see http://nagoya.apache.org/bugzilla/show_bug.cgi?id=5279 340 if (paramValue.length() > 1 341 && paramValue.startsWith("'") 342 && paramValue.endsWith("'")) { 343 paramValue 344 = paramValue.substring (1, paramValue.length() - 1); 345 } 346 347 try { 348 cookie.setExpiryDate(DateParser.parseDate(paramValue)); 349 } catch (DateParseException dpe) { 350 LOG.debug("Error parsing cookie date", dpe); 351 throw new MalformedCookieException( 352 "Unable to parse expiration date parameter: " 353 + paramValue); 354 } 355 } else { 356 if (LOG.isDebugEnabled()) { 357 LOG.debug("Unrecognized cookie attribute: " 358 + attribute.toString()); 359 } 360 } 361 } 362 363 364 /*** 365 * Performs most common {@link Cookie} validation 366 * 367 * @param host the host from which the {@link Cookie} was received 368 * @param port the port from which the {@link Cookie} was received 369 * @param path the path from which the {@link Cookie} was received 370 * @param secure <tt>true</tt> when the {@link Cookie} was received using a 371 * secure connection 372 * @param cookie The cookie to validate. 373 * @throws MalformedCookieException if an exception occurs during 374 * validation 375 */ 376 377 public void validate(String host, int port, String path, 378 boolean secure, final Cookie cookie) 379 throws MalformedCookieException { 380 381 LOG.trace("enter CookieSpecBase.validate(" 382 + "String, port, path, boolean, Cookie)"); 383 if (host == null) { 384 throw new IllegalArgumentException( 385 "Host of origin may not be null"); 386 } 387 if (host.trim().equals("")) { 388 throw new IllegalArgumentException( 389 "Host of origin may not be blank"); 390 } 391 if (port < 0) { 392 throw new IllegalArgumentException("Invalid port: " + port); 393 } 394 if (path == null) { 395 throw new IllegalArgumentException( 396 "Path of origin may not be null."); 397 } 398 if (path.trim().equals("")) { 399 path = PATH_DELIM; 400 } 401 host = host.toLowerCase(); 402 // check version 403 if (cookie.getVersion() < 0) { 404 throw new MalformedCookieException ("Illegal version number " 405 + cookie.getValue()); 406 } 407 408 // security check... we musn't allow the server to give us an 409 // invalid domain scope 410 411 // Validate the cookies domain attribute. NOTE: Domains without 412 // any dots are allowed to support hosts on private LANs that don't 413 // have DNS names. Since they have no dots, to domain-match the 414 // request-host and domain must be identical for the cookie to sent 415 // back to the origin-server. 416 if (host.indexOf(".") >= 0) { 417 // Not required to have at least two dots. RFC 2965. 418 // A Set-Cookie2 with Domain=ajax.com will be accepted. 419 420 // domain must match host 421 if (!host.endsWith(cookie.getDomain())) { 422 String s = cookie.getDomain(); 423 if (s.startsWith(".")) { 424 s = s.substring(1, s.length()); 425 } 426 if (!host.equals(s)) { 427 throw new MalformedCookieException( 428 "Illegal domain attribute \"" + cookie.getDomain() 429 + "\". Domain of origin: \"" + host + "\""); 430 } 431 } 432 } else { 433 if (!host.equals(cookie.getDomain())) { 434 throw new MalformedCookieException( 435 "Illegal domain attribute \"" + cookie.getDomain() 436 + "\". Domain of origin: \"" + host + "\""); 437 } 438 } 439 440 // another security check... we musn't allow the server to give us a 441 // cookie that doesn't match this path 442 443 if (!path.startsWith(cookie.getPath())) { 444 throw new MalformedCookieException( 445 "Illegal path attribute \"" + cookie.getPath() 446 + "\". Path of origin: \"" + path + "\""); 447 } 448 } 449 450 451 /*** 452 * Return <tt>true</tt> if the cookie should be submitted with a request 453 * with given attributes, <tt>false</tt> otherwise. 454 * @param host the host to which the request is being submitted 455 * @param port the port to which the request is being submitted (ignored) 456 * @param path the path to which the request is being submitted 457 * @param secure <tt>true</tt> if the request is using a secure connection 458 * @param cookie {@link Cookie} to be matched 459 * @return true if the cookie matches the criterium 460 */ 461 462 public boolean match(String host, int port, String path, 463 boolean secure, final Cookie cookie) { 464 465 LOG.trace("enter CookieSpecBase.match(" 466 + "String, int, String, boolean, Cookie"); 467 468 if (host == null) { 469 throw new IllegalArgumentException( 470 "Host of origin may not be null"); 471 } 472 if (host.trim().equals("")) { 473 throw new IllegalArgumentException( 474 "Host of origin may not be blank"); 475 } 476 if (port < 0) { 477 throw new IllegalArgumentException("Invalid port: " + port); 478 } 479 if (path == null) { 480 throw new IllegalArgumentException( 481 "Path of origin may not be null."); 482 } 483 if (cookie == null) { 484 throw new IllegalArgumentException("Cookie may not be null"); 485 } 486 if (path.trim().equals("")) { 487 path = PATH_DELIM; 488 } 489 host = host.toLowerCase(); 490 if (cookie.getDomain() == null) { 491 LOG.warn("Invalid cookie state: domain not specified"); 492 return false; 493 } 494 if (cookie.getPath() == null) { 495 LOG.warn("Invalid cookie state: path not specified"); 496 return false; 497 } 498 499 return 500 // only add the cookie if it hasn't yet expired 501 (cookie.getExpiryDate() == null 502 || cookie.getExpiryDate().after(new Date())) 503 // and the domain pattern matches 504 && (domainMatch(host, cookie.getDomain())) 505 // and the path is null or matching 506 && (pathMatch(path, cookie.getPath())) 507 // and if the secure flag is set, only if the request is 508 // actually secure 509 && (cookie.getSecure() ? secure : true); 510 } 511 512 /*** 513 * Performs a domain-match as described in RFC2109. 514 * @param host The host to check. 515 * @param domain The domain. 516 * @return true if the specified host matches the given domain. 517 */ 518 private static boolean domainMatch(String host, String domain) { 519 boolean match = host.equals(domain) 520 || (domain.startsWith(".") && host.endsWith(domain)); 521 522 return match; 523 } 524 525 /*** 526 * Performs a path-match slightly smarter than a straight-forward startsWith 527 * check. 528 * @param path The path to check. 529 * @param topmostPath The path to check against. 530 * @return true if the paths match 531 */ 532 private static boolean pathMatch( 533 final String path, final String topmostPath) { 534 535 boolean match = path.startsWith (topmostPath); 536 537 // if there is a match and these values are not exactly the same we have 538 // to make sure we're not matcing "/foobar" and "/foo" 539 if (match && path.length() != topmostPath.length()) { 540 if (!topmostPath.endsWith(PATH_DELIM)) { 541 match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR); 542 } 543 } 544 return match; 545 } 546 547 /*** 548 * Return an array of {@link Cookie}s that should be submitted with a 549 * request with given attributes, <tt>false</tt> otherwise. 550 * @param host the host to which the request is being submitted 551 * @param port the port to which the request is being submitted (currently 552 * ignored) 553 * @param path the path to which the request is being submitted 554 * @param secure <tt>true</tt> if the request is using a secure protocol 555 * @param cookies an array of <tt>Cookie</tt>s to be matched 556 * @return an array of <tt>Cookie</tt>s matching the criterium 557 */ 558 559 public Cookie[] match(String host, int port, String path, 560 boolean secure, final Cookie cookies[]) { 561 562 LOG.trace("enter CookieSpecBase.match(" 563 + "String, int, String, boolean, Cookie[])"); 564 565 if (host == null) { 566 throw new IllegalArgumentException( 567 "Host of origin may not be null"); 568 } 569 if (host.trim().equals("")) { 570 throw new IllegalArgumentException( 571 "Host of origin may not be blank"); 572 } 573 if (port < 0) { 574 throw new IllegalArgumentException("Invalid port: " + port); 575 } 576 if (path == null) { 577 throw new IllegalArgumentException( 578 "Path of origin may not be null."); 579 } 580 if (cookies == null) { 581 throw new IllegalArgumentException("Cookie array may not be null"); 582 } 583 if (path.trim().equals("")) { 584 path = PATH_DELIM; 585 } 586 host = host.toLowerCase(); 587 588 if (cookies.length <= 0) { 589 return null; 590 } 591 List matching = new LinkedList(); 592 for (int i = 0; i < cookies.length; i++) { 593 if (match(host, port, path, secure, cookies[i])) { 594 addInPathOrder(matching, cookies[i]); 595 } 596 } 597 return (Cookie[]) matching.toArray(new Cookie[matching.size()]); 598 } 599 600 601 /*** 602 * Adds the given cookie into the given list in descending path order. That 603 * is, more specific path to least specific paths. This may not be the 604 * fastest algorythm, but it'll work OK for the small number of cookies 605 * we're generally dealing with. 606 * 607 * @param list - the list to add the cookie to 608 * @param addCookie - the Cookie to add to list 609 */ 610 private static void addInPathOrder(List list, Cookie addCookie) { 611 int i = 0; 612 613 for (i = 0; i < list.size(); i++) { 614 Cookie c = (Cookie) list.get(i); 615 if (addCookie.compare(addCookie, c) > 0) { 616 break; 617 } 618 } 619 list.add(i, addCookie); 620 } 621 622 /*** 623 * Return a string suitable for sending in a <tt>"Cookie"</tt> header 624 * @param cookie a {@link Cookie} to be formatted as string 625 * @return a string suitable for sending in a <tt>"Cookie"</tt> header. 626 */ 627 public String formatCookie(Cookie cookie) { 628 LOG.trace("enter CookieSpecBase.formatCookie(Cookie)"); 629 if (cookie == null) { 630 throw new IllegalArgumentException("Cookie may not be null"); 631 } 632 StringBuffer buf = new StringBuffer(); 633 buf.append(cookie.getName()); 634 buf.append("="); 635 String s = cookie.getValue(); 636 if (s != null) { 637 buf.append(s); 638 }; 639 return buf.toString(); 640 } 641 642 /*** 643 * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in 644 * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header 645 * @param cookies an array of {@link Cookie}s to be formatted 646 * @return a string suitable for sending in a Cookie header. 647 * @throws IllegalArgumentException if an input parameter is illegal 648 */ 649 650 public String formatCookies(Cookie[] cookies) 651 throws IllegalArgumentException { 652 LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])"); 653 if (cookies == null) { 654 throw new IllegalArgumentException("Cookie array may not be null"); 655 } 656 if (cookies.length == 0) { 657 throw new IllegalArgumentException("Cookie array may not be empty"); 658 } 659 660 StringBuffer buffer = new StringBuffer(); 661 for (int i = 0; i < cookies.length; i++) { 662 if (i > 0) { 663 buffer.append("; "); 664 } 665 buffer.append(formatCookie(cookies[i])); 666 } 667 return buffer.toString(); 668 } 669 670 671 /*** 672 * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s 673 * in <i>cookies</i>. 674 * @param cookies an array of {@link Cookie}s to be formatted as a <tt>" 675 * Cookie"</tt> header 676 * @return a <tt>"Cookie"</tt> {@link Header}. 677 */ 678 public Header formatCookieHeader(Cookie[] cookies) { 679 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])"); 680 return new Header("Cookie", formatCookies(cookies)); 681 } 682 683 684 /*** 685 * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}. 686 * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt> 687 * header 688 * @return a Cookie header. 689 */ 690 public Header formatCookieHeader(Cookie cookie) { 691 LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)"); 692 return new Header("Cookie", formatCookie(cookie)); 693 } 694 695 }

This page was automatically generated by Maven