Save This Page
Home » cactus-1.8.0-src » org.apache.cactus.server » [javadoc | source]
    1   /* 
    2    * ========================================================================
    3    * 
    4    * Licensed to the Apache Software Foundation (ASF) under one or more
    5    * contributor license agreements.  See the NOTICE file distributed with
    6    * this work for additional information regarding copyright ownership.
    7    * The ASF licenses this file to You under the Apache License, Version 2.0
    8    * (the "License"); you may not use this file except in compliance with
    9    * the License.  You may obtain a copy of the License at
   10    * 
   11    *   http://www.apache.org/licenses/LICENSE-2.0
   12    * 
   13    * Unless required by applicable law or agreed to in writing, software
   14    * distributed under the License is distributed on an "AS IS" BASIS,
   15    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   16    * See the License for the specific language governing permissions and
   17    * limitations under the License.
   18    * 
   19    * ========================================================================
   20    */
   21   package org.apache.cactus.server;
   22   
   23   import java.io.BufferedReader;
   24   import java.io.File;
   25   import java.io.IOException;
   26   
   27   import java.lang.reflect.Constructor;
   28   
   29   import java.security.Principal;
   30   
   31   import java.util.Enumeration;
   32   import java.util.Locale;
   33   
   34   import javax.servlet.RequestDispatcher;
   35   import javax.servlet.ServletInputStream;
   36   import javax.servlet.http.Cookie;
   37   import javax.servlet.http.HttpServletRequest;
   38   import javax.servlet.http.HttpSession;
   39   
   40   import org.apache.cactus.ServletURL;
   41   import org.apache.cactus.util.ChainedRuntimeException;
   42   
   43   import org.apache.commons.logging.Log;
   44   import org.apache.commons.logging.LogFactory;
   45   
   46   
   47   /**
   48    * Abstract wrapper around {@link HttpServletRequest}. This class provides
   49    * a common implementation of the wrapper for the different Servlet APIs.
   50    * This is an implementation that delegates all the call to the
   51    * {@link HttpServletRequest} object passed in the constructor except for
   52    * some overidden methods which are use to simulate a URL. This is to be able 
   53    * to simulate any URL that would have been used to call the test method : if 
   54    * this was not done, the URL that would be returned (by calling the
   55    * {@link HttpServletRequest#getRequestURI()} method or others alike) would be 
   56    * the URL of the Cactus redirector servlet and not a URL that the test case 
   57    * want to simulate.
   58    *
   59    * @version $Id: AbstractHttpServletRequestWrapper.java 292559 2005-09-29 21:36:43Z kenney $
   60    */
   61   public abstract class AbstractHttpServletRequestWrapper
   62       implements HttpServletRequest
   63   {
   64       /**
   65        * The logger.
   66        */
   67       private static final Log LOGGER = 
   68           LogFactory.getLog(AbstractHttpServletRequestWrapper.class);
   69   
   70       /**
   71        * The real HTTP request.
   72        */
   73       protected HttpServletRequest request;
   74   
   75       /**
   76        * The URL to simulate.
   77        */
   78       protected ServletURL url;
   79   
   80       /**
   81        * Remote IP address to simulate (if any).
   82        * @see #setRemoteIPAddress(String)
   83        */
   84       protected String remoteIPAddress;
   85   
   86       /**
   87        * Remote Host name to simulate (if any).
   88        * @see #setRemoteHostName(String)
   89        */
   90       protected String remoteHostName;
   91   
   92       /**
   93        * Remote user to simulate (if any).
   94        * @see #setRemoteUser(String)
   95        */
   96       protected String remoteUser;
   97   
   98       // New methods not in the interface --------------------------------------
   99   
  100       /**
  101        * Construct an <code>HttpServletRequest</code> instance that delegates
  102        * it's method calls to the request object passed as parameter and that
  103        * uses the URL passed as parameter to simulate a URL from which the request
  104        * would come from.
  105        *
  106        * @param theRequest the real HTTP request
  107        * @param theURL     the URL to simulate or <code>null</code> if none
  108        */
  109       public AbstractHttpServletRequestWrapper(HttpServletRequest theRequest, 
  110           ServletURL theURL)
  111       {
  112           this.request = theRequest;
  113           this.url = theURL;
  114       }
  115       /**
  116        * {@inheritDoc}
  117        * @see HttpServletRequest#newInstance()
  118        */
  119       public static AbstractHttpServletRequestWrapper newInstance(
  120           HttpServletRequest theOriginalRequest, ServletURL theURL)
  121       {
  122           try
  123           {
  124               Class clazz = Class.forName(
  125                   "org.apache.cactus.server.HttpServletRequestWrapper");
  126               Object[] args = new Object[] {theOriginalRequest, theURL};
  127   
  128               Constructor constructor = clazz.getConstructor(new Class[] {
  129                   HttpServletRequest.class, ServletURL.class });
  130   
  131               return (AbstractHttpServletRequestWrapper) constructor.
  132                  newInstance(args);
  133           }
  134           catch (Throwable t)
  135           {
  136               throw new ChainedRuntimeException(
  137                   "Failed to create HttpServletRequestWrapper", t);
  138           }
  139       }
  140   
  141   
  142       /**
  143        * @return the original request object
  144        */
  145       public HttpServletRequest getOriginalRequest()
  146       {
  147           return this.request;
  148       }
  149   
  150       /**
  151        * Simulates the remote IP address (ie the client IP address).
  152        *
  153        * @param theRemoteIPAddress the simulated IP address in string format.
  154        *        Exemple : "127.0.0.1"
  155        */
  156       public void setRemoteIPAddress(String theRemoteIPAddress)
  157       {
  158           this.remoteIPAddress = theRemoteIPAddress;
  159       }
  160   
  161       /**
  162        * Simulates the remote host name(ie the client host name).
  163        *
  164        * @param theRemoteHostName the simulated host name in string format.
  165        *        Exemple : "atlantis"
  166        */
  167       public void setRemoteHostName(String theRemoteHostName)
  168       {
  169           this.remoteHostName = theRemoteHostName;
  170       }
  171   
  172       /**
  173        * Sets the remote user name to simulate.
  174        *
  175        * @param theRemoteUser the simulated remote user name
  176        */
  177       public void setRemoteUser(String theRemoteUser)
  178       {
  179           this.remoteUser = theRemoteUser;
  180       }
  181   
  182       // Modified methods ------------------------------------------------------
  183   
  184       /**
  185        * @return the context path from the simulated URL or the real context path
  186        *         if a simulation URL has not been defined. The real context path
  187        *         will be returned if the context path defined in the simulated 
  188        *         URL has a null value.
  189        */
  190       public String getContextPath()
  191       {
  192           String result = this.request.getContextPath();
  193   
  194           if ((this.url != null) && (this.url.getContextPath() != null))
  195           {
  196               result = this.url.getContextPath();
  197               LOGGER.debug("Using simulated context : [" + result + "]");
  198           }
  199   
  200           return result;
  201       }
  202   
  203       /**
  204        * @return the path info from the simulated URL or the real path info
  205        *         if a simulation URL has not been defined.
  206        */
  207       public String getPathInfo()
  208       {
  209           String result;
  210   
  211           if (this.url != null)
  212           {
  213               result = this.url.getPathInfo();
  214               LOGGER.debug("Using simulated PathInfo : [" + result + "]");
  215           }
  216           else
  217           {
  218               result = this.request.getPathInfo();
  219           }
  220   
  221           return result;
  222       }
  223   
  224       /**
  225        * @return the server name from the simulated URL or the real server name
  226        *         if a simulation URL has not been defined. If the server name
  227        *         defined in the simulation URL is null, return the real server
  228        *         name.
  229        */
  230       public String getServerName()
  231       {
  232           String result = this.request.getServerName();
  233   
  234           if ((this.url != null) && (this.url.getHost() != null))
  235           {
  236               result = this.url.getHost();
  237               LOGGER.debug("Using simulated server name : [" + result + "]");
  238           }
  239   
  240           return result;
  241       }
  242   
  243       /**
  244        * @return the server port number from the simulated URL or the real server
  245        *         port number if a simulation URL has not been defined. If no
  246        *         port is defined in the simulation URL, then port 80 is returned.
  247        *         If the server name has been defined with a null value in
  248        *         in the simulation URL, return the real server port. 
  249        */
  250       public int getServerPort()
  251       {
  252           int result = this.request.getServerPort();
  253   
  254           if ((this.url != null) && (this.url.getServerName() != null))
  255           {
  256               result = (this.url.getPort() == -1) ? 80 : this.url.getPort();
  257               LOGGER.debug("Using simulated server port : [" + result + "]");
  258           }
  259   
  260           return result;
  261       }
  262   
  263       /**
  264        * @return the URI from the simulated URL or the real URI
  265        *         if a simulation URL has not been defined.
  266        */
  267       public String getRequestURI()
  268       {
  269           String result;
  270   
  271           if (this.url != null)
  272           {
  273               result = getContextPath()
  274                   + ((getServletPath() == null) ? "" : getServletPath())
  275                   + ((getPathInfo() == null) ? "" : getPathInfo());
  276   
  277               LOGGER.debug("Using simulated request URI : [" + result + "]");
  278           }
  279           else
  280           {
  281               result = this.request.getRequestURI();
  282           }
  283   
  284           return result;
  285       }
  286   
  287       /**
  288        * @return the servlet path from the simulated URL or the real servlet path
  289        *         if a simulation URL has not been defined. The real servlet path
  290        *         will be returned if the servlet path defined in the simulated 
  291        *         URL has a null value.
  292        */
  293       public String getServletPath()
  294       {
  295           String result = this.request.getServletPath();
  296   
  297           if ((this.url != null) && (this.url.getServletPath() != null))
  298           {
  299               result = this.url.getServletPath();
  300               LOGGER.debug("Using simulated servlet path : [" + result + "]");
  301           }
  302   
  303           return result;
  304       }
  305   
  306       /**
  307        * @return any extra path information after the servlet name but
  308        *         before the query string, and translates it to a real path.
  309        *         Takes into account the simulated URL (if any).
  310        */
  311       public String getPathTranslated()
  312       {
  313           String pathTranslated;
  314   
  315           if ((this.url != null) && (this.url.getPathInfo() != null))
  316           {
  317               String pathInfo = this.url.getPathInfo();
  318               
  319               // If getRealPath returns null then getPathTranslated should also
  320               // return null (see section SRV.4.5 of the Servlet 2.3 spec).
  321               if (this.request.getRealPath("/") == null)
  322               {
  323                   pathTranslated = null;
  324               }
  325               else
  326               {
  327                   // Compute the translated path using the root real path
  328                   String newPathInfo = (pathInfo.startsWith("/")
  329                       ? pathInfo.substring(1) : pathInfo);
  330   
  331                   if (this.request.getRealPath("/").endsWith("/"))
  332                   {
  333                       pathTranslated = this.request.getRealPath("/")
  334                           + newPathInfo.replace('/', File.separatorChar);
  335                   }
  336                   else
  337                   {
  338                       pathTranslated = this.request.getRealPath("/")
  339                           + File.separatorChar + newPathInfo.replace('/', 
  340                           File.separatorChar);
  341                   }
  342               }
  343           }
  344           else
  345           {
  346               pathTranslated = this.request.getPathTranslated();
  347           }
  348   
  349           return pathTranslated;
  350       }
  351   
  352       /**
  353        * @return the query string from the simulated URL or the real query
  354        *         string if a simulation URL has not been defined.
  355        */
  356       public String getQueryString()
  357       {
  358           String result;
  359   
  360           if (this.url != null)
  361           {
  362               result = this.url.getQueryString();
  363               LOGGER.debug("Using simulated query string : [" + result + "]");
  364           }
  365           else
  366           {
  367               result = this.request.getQueryString();
  368           }
  369   
  370           return result;
  371       }
  372   
  373       /**
  374        * @param thePath the path to the resource
  375        * @return a wrapped request dispatcher instead of the real one, so that
  376        *         forward() and include() calls will use the wrapped dispatcher
  377        *         passing it the *original* request [this is needed for some
  378        *         servlet engine like Tomcat 3.x which do not support the new
  379        *         mechanism introduced by Servlet 2.3 Filters].
  380        * @see HttpServletRequest#getRequestDispatcher(String)
  381        */
  382       public RequestDispatcher getRequestDispatcher(String thePath)
  383       {
  384           // I hate it, but we have to write some logic here ! Ideally we
  385           // shouldn't have to do this as it is supposed to be done by the servlet
  386           // engine. However as we are simulating the request URL, we have to
  387           // provide it ... This is where we can see the limitation of Cactus
  388           // (it has to mock some parts of the servlet engine) !
  389           if (thePath == null)
  390           {
  391               return null;
  392           }
  393   
  394           RequestDispatcher dispatcher = null;
  395           String fullPath;
  396   
  397           // The spec says that the path can be relative, in which case it will
  398           // be relative to the request. So for relative paths, we need to take
  399           // into account the simulated URL (ServletURL).
  400           if (thePath.startsWith("/"))
  401           {
  402               fullPath = thePath;
  403           }
  404           else
  405           {
  406               String pI = getPathInfo();
  407   
  408               if (pI == null)
  409               {
  410                   fullPath = catPath(getServletPath(), thePath);
  411               }
  412               else
  413               {
  414                   fullPath = catPath(getServletPath() + pI, thePath);
  415               }
  416   
  417               if (fullPath == null)
  418               {
  419                   return null;
  420               }
  421           }
  422   
  423           LOGGER.debug("Computed full path : [" + fullPath + "]");
  424   
  425           dispatcher = new RequestDispatcherWrapper(
  426               this.request.getRequestDispatcher(fullPath));
  427   
  428           return dispatcher;
  429       }
  430   
  431       /**
  432        * Will concatenate 2 paths, normalising it. For example :
  433        * ( /a/b/c + d = /a/b/d, /a/b/c + ../d = /a/d ). Code borrowed from
  434        * Tomcat 3.2.2 !
  435        *
  436        * @param theLookupPath the first part of the path
  437        * @param thePath the part to add to the lookup path
  438        * @return the concatenated thePath or null if an error occurs
  439        */
  440       private String catPath(String theLookupPath, String thePath)
  441       {
  442           // Cut off the last slash and everything beyond
  443           int index = theLookupPath.lastIndexOf("/");
  444   
  445           theLookupPath = theLookupPath.substring(0, index);
  446   
  447           // Deal with .. by chopping dirs off the lookup thePath
  448           while (thePath.startsWith("../"))
  449           {
  450               if (theLookupPath.length() > 0)
  451               {
  452                   index = theLookupPath.lastIndexOf("/");
  453                   theLookupPath = theLookupPath.substring(0, index);
  454               }
  455               else
  456               {
  457                   // More ..'s than dirs, return null
  458                   return null;
  459               }
  460   
  461               index = thePath.indexOf("../") + 3;
  462               thePath = thePath.substring(index);
  463           }
  464   
  465           return theLookupPath + "/" + thePath;
  466       }
  467   
  468       /**
  469        * @return the simulated remote IP address if any or the real one.
  470        *
  471        * @see HttpServletRequest#getRemoteAddr()
  472        */
  473       public String getRemoteAddr()
  474       {
  475           String remoteIPAddress;
  476   
  477           if (this.remoteIPAddress != null)
  478           {
  479               remoteIPAddress = this.remoteIPAddress;
  480           }
  481           else
  482           {
  483               remoteIPAddress = this.request.getRemoteAddr();
  484           }
  485   
  486           return remoteIPAddress;
  487       }
  488   
  489       /**
  490        * @return the simulated remote host name if any or the real one.
  491        *
  492        * @see HttpServletRequest#getRemoteHost()
  493        */
  494       public String getRemoteHost()
  495       {
  496           String remoteHostName;
  497   
  498           if (this.remoteHostName != null)
  499           {
  500               remoteHostName = this.remoteHostName;
  501           }
  502           else
  503           {
  504               remoteHostName = this.request.getRemoteHost();
  505           }
  506   
  507           return remoteHostName;
  508       }
  509   
  510       /**
  511        * @return the simulated remote user name if any or the real one.
  512        *
  513        * @see HttpServletRequest#getRemoteUser()
  514        */
  515       public String getRemoteUser()
  516       {
  517           String remoteUser;
  518   
  519           if (this.remoteUser != null)
  520           {
  521               remoteUser = this.remoteUser;
  522           }
  523           else
  524           {
  525               remoteUser = this.request.getRemoteUser();
  526           }
  527   
  528           return remoteUser;
  529       }
  530   
  531       // Not modified methods --------------------------------------------------
  532   
  533       /**
  534        * {@inheritDoc}
  535        * @see HttpServletRequest#isRequestedSessionIdFromURL()
  536        */
  537       public boolean isRequestedSessionIdFromURL()
  538       {
  539           return this.request.isRequestedSessionIdFromURL();
  540       }
  541   
  542       /**
  543        * {@inheritDoc} 
  544        * @see HttpServletRequest#isRequestedSessionIdFromUrl()
  545        */
  546       public boolean isRequestedSessionIdFromUrl()
  547       {
  548           return this.request.isRequestedSessionIdFromURL();
  549       }
  550   
  551       /**
  552        * {@inheritDoc}
  553        * @see HttpServletRequest#isUserInRole(String)
  554        */
  555       public boolean isUserInRole(String theRole)
  556       {
  557           return this.request.isUserInRole(theRole);
  558       }
  559   
  560       /**
  561        * {@inheritDoc}
  562        * @see HttpServletRequest#isRequestedSessionIdValid()
  563        */
  564       public boolean isRequestedSessionIdValid()
  565       {
  566           return this.request.isRequestedSessionIdValid();
  567       }
  568   
  569       /**
  570        * {@inheritDoc}
  571        * @see HttpServletRequest#isRequestedSessionIdFromCookie()
  572        */
  573       public boolean isRequestedSessionIdFromCookie()
  574       {
  575           return this.request.isRequestedSessionIdFromCookie();
  576       }
  577   
  578       /**
  579        * {@inheritDoc}
  580        * @see HttpServletRequest#getLocales()
  581        */
  582       public Enumeration getLocales()
  583       {
  584           return this.request.getLocales();
  585       }
  586   
  587       /**
  588        * {@inheritDoc}
  589        * @see HttpServletRequest#getHeader(String)
  590        */
  591       public String getHeader(String theName)
  592       {
  593           return this.request.getHeader(theName);
  594       }
  595   
  596       /**
  597        * {@inheritDoc}
  598        * @see HttpServletRequest#getHeaders(String)
  599        */
  600       public Enumeration getHeaders(String theName)
  601       {
  602           return this.request.getHeaders(theName);
  603       }
  604   
  605       /**
  606        * {@inheritDoc}
  607        * @see HttpServletRequest#getHeaderNames()
  608        */
  609       public Enumeration getHeaderNames()
  610       {
  611           return this.request.getHeaderNames();
  612       }
  613   
  614       /**
  615        * {@inheritDoc}
  616        * @see HttpServletRequest#getScheme()
  617        */
  618       public String getScheme()
  619       {
  620           return this.request.getScheme();
  621       }
  622   
  623       /**
  624        * {@inheritDoc}
  625        * @see HttpServletRequest#getAuthType()
  626        */
  627       public String getAuthType()
  628       {
  629           return this.request.getAuthType();
  630       }
  631   
  632       /**
  633        * {@inheritDoc}
  634        * @see HttpServletRequest#getRealPath(String)
  635        */
  636       public String getRealPath(String thePath)
  637       {
  638           return this.request.getRealPath(thePath);
  639       }
  640   
  641       /**
  642        * {@inheritDoc}
  643        * @see HttpServletRequest#getSession()
  644        */
  645       public HttpSession getSession()
  646       {
  647           return this.request.getSession();
  648       }
  649   
  650       /**
  651        * {@inheritDoc}
  652        * @see HttpServletRequest#getSession(boolean)
  653        */
  654       public HttpSession getSession(boolean isCreate)
  655       {
  656           return this.request.getSession(isCreate);
  657       }
  658   
  659       /**
  660        * {@inheritDoc}
  661        * @see HttpServletRequest#getReader()
  662        */
  663       public BufferedReader getReader() throws IOException
  664       {
  665           return this.request.getReader();
  666       }
  667   
  668       /**
  669        * {@inheritDoc}
  670        * @see HttpServletRequest#getContentLength()
  671        */
  672       public int getContentLength()
  673       {
  674           return this.request.getContentLength();
  675       }
  676   
  677       /**
  678        * {@inheritDoc}
  679        * @see HttpServletRequest#getParameterValues(String)
  680        */
  681       public String[] getParameterValues(String theName)
  682       {
  683           return this.request.getParameterValues(theName);
  684       }
  685   
  686       /**
  687        * {@inheritDoc}
  688        * @see HttpServletRequest#getContentType()
  689        */
  690       public String getContentType()
  691       {
  692           return this.request.getContentType();
  693       }
  694   
  695       /**
  696        * {@inheritDoc}
  697        * @see HttpServletRequest#getLocale()
  698        */
  699       public Locale getLocale()
  700       {
  701           return this.request.getLocale();
  702       }
  703   
  704       /**
  705        * {@inheritDoc}
  706        * @see HttpServletRequest#removeAttribute(String)
  707        */
  708       public void removeAttribute(String theName)
  709       {
  710           this.request.removeAttribute(theName);
  711       }
  712   
  713       /**
  714        * {@inheritDoc}
  715        * @see HttpServletRequest#getParameter(String)
  716        */
  717       public String getParameter(String theName)
  718       {
  719           return this.request.getParameter(theName);
  720       }
  721   
  722       /**
  723        * {@inheritDoc}
  724        * @see HttpServletRequest#getInputStream()
  725        */
  726       public ServletInputStream getInputStream() throws IOException
  727       {
  728           return this.request.getInputStream();
  729       }
  730   
  731       /**
  732        * {@inheritDoc}
  733        * @see HttpServletRequest#getUserPrincipal()
  734        */
  735       public Principal getUserPrincipal()
  736       {
  737           return this.request.getUserPrincipal();
  738       }
  739   
  740       /**
  741        * {@inheritDoc}
  742        * @see HttpServletRequest#isSecure()
  743        */
  744       public boolean isSecure()
  745       {
  746           return this.request.isSecure();
  747       }
  748   
  749       /**
  750        * {@inheritDoc}
  751        * @see HttpServletRequest#getCharacterEncoding()
  752        */
  753       public String getCharacterEncoding()
  754       {
  755           return this.request.getCharacterEncoding();
  756       }
  757   
  758       /**
  759        * {@inheritDoc}
  760        * @see HttpServletRequest#getParameterNames()
  761        */
  762       public Enumeration getParameterNames()
  763       {
  764           return this.request.getParameterNames();
  765       }
  766   
  767       /**
  768        * {@inheritDoc}
  769        * @see HttpServletRequest#getMethod()
  770        */
  771       public String getMethod()
  772       {
  773           return this.request.getMethod();
  774       }
  775   
  776       /**
  777        * {@inheritDoc}
  778        * @see HttpServletRequest#setAttribute(String, Object)
  779        */
  780       public void setAttribute(String theName, Object theAttribute)
  781       {
  782           this.request.setAttribute(theName, theAttribute);
  783       }
  784   
  785       /**
  786        * {@inheritDoc}
  787        * @see HttpServletRequest#getAttribute(String)
  788        */
  789       public Object getAttribute(String theName)
  790       {
  791           return this.request.getAttribute(theName);
  792       }
  793   
  794       /**
  795        * {@inheritDoc}
  796        * @see HttpServletRequest#getIntHeader(String)
  797        */
  798       public int getIntHeader(String theName)
  799       {
  800           return this.request.getIntHeader(theName);
  801       }
  802   
  803       /**
  804        * {@inheritDoc}
  805        * @see HttpServletRequest#getDateHeader(String)
  806        */
  807       public long getDateHeader(String theName)
  808       {
  809           return this.request.getDateHeader(theName);
  810       }
  811   
  812       /**
  813        * {@inheritDoc}
  814        * @see HttpServletRequest#getAttributeNames()
  815        */
  816       public Enumeration getAttributeNames()
  817       {
  818           return this.request.getAttributeNames();
  819       }
  820   
  821       /**
  822        * {@inheritDoc}
  823        * @see HttpServletRequest#getRequestedSessionId()
  824        */
  825       public String getRequestedSessionId()
  826       {
  827           return this.request.getRequestedSessionId();
  828       }
  829   
  830       /**
  831        * {@inheritDoc}
  832        * @see HttpServletRequest#getCookies()
  833        */
  834       public Cookie[] getCookies()
  835       {
  836           return this.request.getCookies();
  837       }
  838   
  839       /**
  840        * {@inheritDoc}
  841        * @see HttpServletRequest#getProtocol()
  842        */
  843       public String getProtocol()
  844       {
  845           return this.request.getProtocol();
  846       }
  847   }

Save This Page
Home » cactus-1.8.0-src » org.apache.cactus.server » [javadoc | source]