RestHandler.java

/*
** Module   : RestHandler.java
** Abstract : Handler for REST calls.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------------------------------Description--------------------------------
** 001 CA  20190611 First version.
** 002 CA  20190628 Proper SINGLETON and SINGLE-RUN implementation; misc improvements.
**     CA  20190703 Extracted common code to LegacyServiceHandler.
** 003 CA  20190723 The path matching is done case-insensitive.
** 004 CA  20200427 Fixed to allow session context details.
**                  Added application/x-www-form-urlencoded support.
**     CA  20200514 Refactoring to allow common code for SOAP services support.
** 005 CA  20200910 The 'address' at the service signature is included in the key.
**     CA  20210227 Each specific 'address' must have its own handler, so the context path can be reported
**                  and managed properly.
**     CA  20211112 Fixed some problems resetting the last invoke configuration.
**                  Fixed setting the status - it must be set before the response body is sent.
**     CA  20220329 Added support for REST services written directly in Java.
**     CA  20220405 Added authentication and authorization for web requests.  When this is enabled, the target
**                  API call will be executed under the authenticated FWD context, and not the agent's context.
**     SBI 20221215 Changed buildServiceKey to provide parameters types as new parameter for RestServiceKey.
**     SBI 20221216 Choose the target rest service by comparing its context path with a request context path.
**     SBI 20221229 Reduced the number of iterations until the target request handler would be found.            
** 006 SBI 20230216 Fixed buildJavaParameter(..) because defaultValue() was added to LegacyServiceParameter,
**                  changed invokeRestService,...) to handle RestException.
** 007 SBI 20230324 Fixed legacy rest services to be added to the rest service context map.
** 008 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 009 GBB 20240610 LegacyAppServerWork replaced by Consumer<LegacyServiceWorker>. 
** 010 SBI 20231206 Resolved conflicting rest service methods with help of the priority queue.
** 011 GBB 20250403 Replace service type with enum constant.
** 012 RB2 20250530 The handle method now calls the handle method with an extra parameter, the request. This
**                  was done in order to handle multiple URLs in the Access-Control-Allow-Origin field
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.rest;

import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;

import javax.servlet.http.*;

import com.goldencode.p2j.util.logging.*;
import org.eclipse.jetty.http.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;

import com.goldencode.p2j.main.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.util.*;

/**
 * Processes the REST requests and delegates the calls to the mapped service.
 * <p>
 * Service registration is being performed via {@link SourceNameMapper}, which loads the
 * programs marked with <code>with-services</code>, via {@link #addService} API.
 * <p>
 * Any converted Java code mapped to a service must have a class-level {@link LegacyService} 
 * annotation.  If this has operations defined via internal entries (procedure, function, method),
 * then each Java method must be annotated via {@link LegacyService}.
 * <p>
 * The implicit basepath is <code>/rest</code>, and the implicit timeout is 10 seconds (set to 0
 * to disable it).  This timeout is read from the appserver's timeout.
 * <p>
 * The values which need to be configured in the directory are:
 * <ul>
 *    <li><code>rest/basepath</code>, to specify the basepath.  Defaults to <code>/rest</code>.</li>
 *    <li><code>rest/alias</code>, a process certificate alias associated with the workers which
 *        will process the REST requests; these will delegate the call to the appserver.  This
 *        context will automatically be set as HEADLESS.</li>
 *    <li><code>rest/keyStore</code>, the key store with the certificate to authenticate this 
 *        process.  If is missing, the store will be built from the directory.</li>
 *    <li><code>rest/storePassword</code>, the store password, only if <code>rest/keyStore</code>
 *        is set.</li>
 *    <li><code>rest/poolSize</code> - the pool size of workers.  Defaults to 10.</li>
 *    <li><code>rest/appserver</code> - the name of the appserver handling the requests.</li>
 * </ul>
 * <p>
 * Each queue of REST workers is ordered by the number of pending tasks to be executed - for each
 * incoming request, the worker with the smallest number of pending tasks will be chosen.
 */
public class RestHandler
extends LegacyWebServiceHandler
{
   /** The singleton instance for this handler. */
   private static RestHandler instance = null;

   /** Anonymous log instance. */
   private static final CentralLogger LOG = CentralLogger.get(RestHandler.class.getName());
   
   /** A mapping of mime-types to their request resolvers. */
   private static Map<String, Supplier<? extends RequestArguments>> requests = new HashMap<>();
   
   /** A mapping of mime-types to their response serializers. */
   private static Map<String, Supplier<? extends ResponseArguments>> responses = new HashMap<>();
   
   /** The context handler. */
   private static ContextHandlerCollection handler;
   
   /** The already registered addresses. */
   private Set<String> addresses = new HashSet<>();
   
   /** Map context path to rest services registered with the same context path */
   private Map<String, Map<ServiceKey, RestService>> restServices = new HashMap<>();
   
   /**
    * Initialize this handler with the 'rest' type and '/rest' default basepath.
    */
   public RestHandler()
   {
      super(ServiceType.REST, "/rest");
   }

   /**
    * Determine if this web service is configured to authenticate the APIs.
    * 
    * @return   <code>true</code> if the authentication is enabled for this web service, in the directory.
    */
   public static boolean useWebServiceAuthentication()
   {
      synchronized (RestHandler.class)
      {
         return instance != null && instance.useAuthorization();
      }
   }
   
   /**
    * Terminate the rest worker threads.
    */
   public static void terminate()
   {
      synchronized (RestHandler.class)
      {
         if (instance != null)
         {
            instance.terminateWorkers();
         }
      }
   }

   /**
    * Initialize the REST worker(s) and start the workers.
    * 
    * @return   The handler for REST services.
    */
   public static AbstractHandler initialize()
   {
      synchronized (RestHandler.class)
      {
         if (handler != null)
         {
            return handler;
         }
         
         instance = new RestHandler();
         instance.initializeHandler();
         
         requests.put("application/x-www-form-urlencoded", () -> new BaseRequestsParser());
         requests.put("application/json", () -> new JsonRequestsParser());
         responses.put("application/json", () -> new JsonResponseArguments());

         handler = new ContextHandlerCollection();
         
         return handler;
      }
   }

   /**
    * Register a service which is defined in Java, and does not adhere to the constraints of a legacy program.
    * 
    * @param    ls
    *           The annotation describing the service, at the defining class.
    * @param    cls
    *           The Java class defining this service.
    * @param    mls
    *           The annotation describing the service, at the Java method.
    * @param    m
    *           The target Java method.
    */
   public static void addJavaService(LegacyService ls,
                                     Class<?>      cls,
                                     LegacyService mls,
                                     Method        m)
   {
      synchronized (RestHandler.class)
      {
         if (instance == null)
         {
            LOG.warning("Trying to register REST service " + mls.name() + 
                               ", but REST is not enabled.");
            return;
         }
      }
      
      instance.registerJavaService(ls, cls, mls, m);
   }
   
   /**
    * Register a legacy service associated with a legacy OO class method.
    * 
    * @param    ls
    *           The annotation describing the service, at the legacy class.
    * @param    cls
    *           The {@link com.goldencode.p2j.oo.lang.BaseObject} sub-class implementing this
    *           service.
    * @param    mls
    *           The annotation describing the service, at the legacy method.
    * @param    m
    *           The target operation.
    */
   public static void addService(LegacyService                 ls,
                                 Class<? extends _BaseObject_> cls,
                                 LegacyService                 mls,
                                 Method                        m)
   {
      synchronized (RestHandler.class)
      {
         if (instance == null)
         {
            LOG.warning("Trying to register REST service " + mls.name() + 
                               ", but REST is not enabled.");
            return;
         }
      }

      RestService srv = instance.registerService(ls, cls, mls, m);
      instance.addToContextMap(srv);
   }

   /**
    * Register a legacy service associated with a legacy external program.
    * 
    * @param    ls
    *           The annotation describing the service.
    * @param    pname
    *           The legacy program name.
    * @param    m
    *           The target Java method for this service.
    */
   public static void addService(LegacyService ls, String pname, Method m)
   {
      synchronized (RestHandler.class)
      {
         if (instance == null)
         {
            LOG.warning("Trying to register REST service " + pname + 
                               ", but REST is not enabled.");
            return;
         }
      }
      
      RestService srv = instance.registerService(ls, pname, m);
      instance.addToContextMap(srv);
   }

   /**
    * Register a legacy service associated with an internal entry for an external program.
    * 
    * @param    ls
    *           The annotation describing the service, at the external program.
    * @param    pname
    *           The legacy program name.
    * @param    iels
    *           The annotation describing the service, at the internal entry.
    * @param    ie
    *           The target internal entry for this service.
    * @param    function
    *           Flag identifying if the internal entry is a function.
    */
   public static void addService(LegacyService  ls,
                                 String         pname, 
                                 LegacyService  iels, 
                                 InternalEntry  ie, 
                                 boolean        function)
   {
      synchronized (RestHandler.class)
      {
         if (instance == null)
         {
            LOG.warning("Trying to register REST service " + pname + "." + ie.getLegacyName() + 
                               ", but REST is not enabled.");
            return;
         }
      }
      
      RestService srv = instance.registerService(ls, pname, iels, ie, function);
      instance.addToContextMap(srv);
   }
   
   /**
    * From a structure like <code>a.attr['a']</code>, extract the bracketed value and remove the
    * quotes.
    * 
    * @param    src
    *           The source text.
    *           
    * @return   The bracketed value.
    */
   static String extractVal(String src)
   {
      String val = src.substring(src.indexOf('[') + 1);
      val = val.substring(0, val.lastIndexOf(']'));
      if (val.startsWith("'") || val.startsWith("\""))
      {
         val = val.substring(1, val.length() - 1);
      }
      
      return val;
   }
   
   /**
    * Execute the REST request.
    * 
    * @param    rest
    *           The REST service instance.
    * @param    token
    *           When not null, it represents the token of a FWD context created for an authenticated and 
    *           authorized web service call.  The API call will be performed in that context.
    * @param    worker
    *           The REST worker.
    * @param    target
    *           The target path.
    * @param    request
    *           The HTTP servlet request instance.
    * @param    response
    *           The HTTP servlet response instance.
    */
   private static void invoke(RestService         rest,
                              String              token,
                              LegacyServiceWorker worker,
                              String              target, 
                              HttpServletRequest  request,
                              HttpServletResponse response)
   throws IOException
   {
      OutputStream stream = response.getOutputStream();
      response.setContentType(rest.ls.produces());

      RequestArguments rs = requests.get(rest.ls.consumes()).get();
      ResponseArguments rw = responses.get(rest.ls.produces()).get();

      try
      {
         try
         {
            AppServerHelper helper = worker.getHelper();

            Object[] args = rs.parseArguments(rest, request);

            Object retVal = rest.invoke(helper, token, args);
   
            rw.writeArguments(retVal, args, rest, stream, response);
         }
         catch (Throwable exc)
         {
            LOG.log(Level.SEVERE, "Could not execute " + target, exc);
            
            Throwable t = exc;
            while (t != null)
            {
               if (t instanceof LegacyErrorException)
               {
                  LegacyErrorException lex = (LegacyErrorException) t;
                  LegacyError error = lex.getErrorRef();

                  response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
                  
                  // write the error to JSON
                  String json = rw.writeError(error);
                  stream.write(json.getBytes());
                  return;
               }
               else if (t instanceof RestException)
               {
                  response.setStatus(((RestException) t).getStatus());
                  t.printStackTrace(new PrintStream(stream));
                  return;
               }
               if (t instanceof ErrorConditionException)
               {
                  if (t.getCause() instanceof StopConditionException)
                  {
                     throw (StopConditionException) t.getCause();
                  }
                  else
                  {
                     // String msg = "ERROR condition: " + t.getMessage();
                     response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
                     String msg = "ERROR condition: The Server application has returned an error. (7243) (7211)";
                     stream.write(msg.getBytes());
                  }
                  return;
               }
               else if (t instanceof ConditionException)
               {
                  throw (ConditionException) t;
               }
               else if (t instanceof RequestArgumentError)
               {
                  throw (RequestArgumentError) t;
               }
               
               t = t.getCause();
            }
            
            // other failure?
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
            stream.write(exc.getMessage().getBytes());
         }
         finally
         {
            // reset the config
            rest.reset();
         }
      }
      catch (QuitConditionException |
             StopConditionException e)
      {
         response.setStatus(HttpStatus.GATEWAY_TIMEOUT_504);
      }
      catch (ConditionException e)
      {
         response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
         stream.write(e.getMessage().getBytes());
      }
      catch (RequestArgumentError e)
      {
         response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
         stream.write(e.getMessage().getBytes());
      }
   }

   /**
    * Code to process web requests.
    */
   @Override
   public void handle(String              target, 
                      Request             baseRequest,
                      HttpServletRequest  request,
                      HttpServletResponse response) 
   throws IOException
   {
      if (baseRequest.isHandled())
      {
         return;
      }

      // first check the authentication
      super.handle(target, baseRequest, request, response);

      if (baseRequest.isHandled())
      {
         // authentication failed, nothing else to do
         return;
      }
      
      baseRequest.setHandled(true);
      
      String[] targetPaths = getPaths(target);
      
      String method = request.getMethod();
      PriorityQueue<RestServiceKey> candidates = new PriorityQueue<RestServiceKey>(new Comparator<RestServiceKey>()
      {
         public int compare(RestServiceKey o1, RestServiceKey o2)
         {
            int size = Math.min(o1.mask.size(), o2.mask.size());
            for(int i = 0; i < size; i++)
            {
               if (o1.mask.get(i) != o2.mask.get(i))
               {
                  if (o1.mask.get(i))
                  {
                     return -i - 1;
                  }
                  else
                  {
                     return i + 1;
                  }
               }
            }
            
            return 0;
         }
      });
      Map<ServiceKey, RestService> contextServices = restServices.getOrDefault(request.getContextPath(), Collections.EMPTY_MAP);
      
      for (ServiceKey skey : contextServices.keySet())
      {
         RestServiceKey key = (RestServiceKey) skey;
         if (method.equals(key.verb))
         {
            if (key.paths.length == targetPaths.length)
            {
               boolean match = true;
               BitSet mask = key.mask;
               // iterate over all none wildcards elements in the rest path
               for (int i = mask.nextSetBit(0); i >= 0; i = mask.nextSetBit(i + 1))
               {
                  String p = key.paths[i];
                  if (!p.equalsIgnoreCase(targetPaths[i]))
                  {
                     match = false;
                     break;
                  }
                  // operate on index i here
                  if (i == Integer.MAX_VALUE)
                  {
                     break; // or (i+1) would overflow
                  }
               }
               
               if (match)
               {
                  candidates.add(key);
               }
            }
         }
      }
      
      if (candidates.isEmpty())
      {
         // TODO: we need to differentiate between a service configured in the .paar definition,
         // and a real missing program.
         response.setStatus(HttpStatus.NOT_FOUND_404);
         response.getOutputStream().write(("Can't find service for path " + target).getBytes());
         return;
      }
      
      RestService rest = contextServices.get(candidates.peek());
      LegacyAppServerWork work = (worker) -> 
      {
         // now do the authorization - must be in the worker thread
         String token;
         if (useAuthorization())
         {
            token = authorize(target, request, response);
            if (token == null)
            {
               return;
            }
         }
         else
         {
            token = null;
         }
         
         try
         {
            invoke(rest, token, worker, target, request, response);
         }
         catch (IOException e)
         {
            throw new RuntimeException(e);
         }
         finally
         {
            postHandle(token);
         }
      };
      
      handle(this, work, target, response, request);
   }
   
   /**
    * Build a {@link RestServiceKey} for the specified legacy service.
    * 
    * @param    ls
    *           The legacy service annotation.
    * 
    * @return   The service key.
    */
   @Override
   protected RestServiceKey buildServiceKey(LegacyService ls)
   {
      java.util.stream.Stream<LegacyServiceParameter> legacyParametersStream = Arrays.stream(ls.parameters());
      List<String> typesList = new ArrayList<String>(ls.parameters().length);
      legacyParametersStream.map(lp -> {return lp.type();}).forEach(tp -> typesList.add(tp));
      RestServiceKey rsk = new RestServiceKey(this.basePath, ls.address(), ls.path(), ls.verb(), typesList);
      if (!addresses.contains(rsk.address))
      {
         addresses.add(rsk.address);

         ContextHandler keyHandler = new ContextHandler();
         String path = getBasePath() + rsk.address;
         while (path.indexOf("//") >= 0)
         {
            path = path.replaceAll("//", "/");
         }
         if (path.endsWith("/"))
         {
            path = path.substring(0, path.length() - 1);
         }
         keyHandler.setContextPath(path);
         keyHandler.setHandler(this);
         
         handler.addHandler(keyHandler);
      }

      return rsk;
   }
   
   /**
    * Register a service which is defined in Java, and does not adhere to the constraints of a legacy program.
    * 
    * @param    ls
    *           The annotation describing the service, at the defining class.
    * @param    cls
    *           The Java class defining this service.
    * @param    mls
    *           The annotation describing the service, at the Java method.
    * @param    m
    *           The target Java method.
    */
   private void registerJavaService(LegacyService ls,
                                    Class<?>      cls,
                                    LegacyService mls,
                                    Method        m)
   {
      // re-build the parameters and method's service annotation, by:
      // - inheriting any 'address', 'produces', 'consumes' set at the class
      // - getting the argument name and type from the Java method's signature
      BiFunction<LegacyService, LegacyServiceParameter[], LegacyService> buildService = (orig, params) ->
      {
         return new LegacyService()
         {
            @Override
            public Class<? extends Annotation> annotationType()
            {
               return LegacyService.class;
            }
   
            @Override
            public String type()
            {
               return orig.type();
            }
   
            @Override
            public LegacyWebPath[] paths()
            {
               return orig.paths();
            }
   
            @Override
            public String namespace()
            {
               return orig.namespace();
            }
   
            @Override
            public int order()
            {
               return orig.order();
            }
   
            @Override
            public String executionMode()
            {
               return orig.executionMode();
            }
   
            @Override
            public String name()
            {
               return orig.name();
            }
   
            @Override
            public String path()
            {
               return orig.path();
            }
   
            @Override
            public String address()
            {
               return orig.address().isEmpty() ? ls.address() : orig.address();
            }
   
            @Override
            public String verb()
            {
               return orig.verb();
            }
   
            @Override
            public String produces()
            {
               return orig.produces().isEmpty() ? ls.produces() : orig.produces();
            }
   
            @Override
            public String consumes()
            {
               return orig.consumes().isEmpty() ? ls.consumes() : orig.consumes();
            }
   
            @Override
            public LegacyServiceParameter[] parameters()
            {
               return params;
            }
   
            @Override
            public String binding()
            {
               return orig.binding();
            }
         };
      };
      
      Type retType = m.getGenericReturnType();
      Parameter[] params = m.getParameters();
      LegacyServiceParameter[] defParams = mls.parameters();
      LegacyServiceParameter retParam = null;
      LegacyServiceParameter[] newParams = new LegacyServiceParameter[params.length];
      
      for (int i = 0; i < defParams.length; i++)
      {
         LegacyServiceParameter lsp = defParams[i];
         
         if ((lsp.output() || lsp.returnValue()) && lsp.target().isEmpty())
         {
            throw new IllegalStateException("Parameter " + lsp.ordinal() + " in " + m.toString() + 
                                            " is marked OUTPUT, but no TARGET is set.");
         }
         if (lsp.input() && lsp.source().isEmpty())
         {
            throw new IllegalStateException("Parameter " + lsp.ordinal() + " in " + m.toString() + 
                                            " is marked INPUT, but no SOURCE is set.");
         }
         if (lsp.input() && !lsp.output() && !lsp.target().isEmpty())
         {
            throw new IllegalStateException("Parameter " + lsp.ordinal() + " in " + m.toString() + 
                                            " is marked INPUT, and TARGET is set.");
         }
         if ((lsp.output() || lsp.returnValue()) && !lsp.input() && !lsp.source().isEmpty())
         {
            throw new IllegalStateException("Parameter " + lsp.ordinal() + " in " + m.toString() + 
                                            " is marked OUTPUT, and SOURCE is set.");
         }

         if (lsp.returnValue())
         {
            retParam = buildJavaParameter(lsp, retType, "retVal");
            continue;
         }
         
         int idx = lsp.ordinal() - 1;
         Type ptype = params[idx].getParameterizedType();
         String pname = params[idx].getName();
         newParams[idx] = buildJavaParameter(lsp, ptype, pname);
      }
      
      if (retParam != null && (retType == void.class || retType == Void.class))
      {
         throw new RuntimeException("'returnValue = true' parameter can't be used for method " + m.getName() + 
                                    " in class " + cls.getName() + ", which has a 'void' return type!");
      }
      for (int i = 0; i < newParams.length; i++)
      {
         if (newParams[i] == null)
         {
            throw new RuntimeException("Parameter " + (i + 1) + " at the " + m.getName() + " in " + 
                                       cls.getName() + " must be defined at the LegacyService!");
         }
      }
      
      if (retParam != null)
      {
         LegacyServiceParameter[] withRetVal = new LegacyServiceParameter[params.length + 1];
         withRetVal[0] = retParam;
         System.arraycopy(newParams, 0, withRetVal, 1, params.length);
         newParams = withRetVal;
      }
      
      LegacyService newService = buildService.apply(mls, newParams);
      
      // create a JavaRestService and set at it:
      // - the target class
      // - the target method
      // - the method's LegacySignature
      // - the method's return LegacyServiceParameter
      RestServiceKey key = buildServiceKey(newService);

      if (services.containsKey(key))
      {
         throw new RuntimeException("More than one " + getType().toUpperCase() + " service mapped to " + key);
      }

      RestService srv = new JavaRestService(cls, m, newService, key, getTimeout());
      services.put(srv.key, srv);
      
      addToContextMap(srv);
   }
   
   /**
    * Add the given rest service to its context path map.
    * 
    * @param    srv
    *           The rest service
    */
   private void addToContextMap(RestService srv)
   {
      RestServiceKey key = (RestServiceKey) srv.key;
      Map<ServiceKey, RestService> contextServices = restServices.get(key.context);
      if (contextServices == null)
      {
         contextServices = new HashMap<>();
         restServices.put(key.context, contextServices);
      }
      contextServices.put(key, srv);
   }
   
   /**
    * Build the REST service parameter associated with a Java-style defined service.
    * 
    * @param    orig
    *           The original signature.
    * @param    type
    *           The parameter's type, as defined at the Java method.
    * @param    name
    *           The parameter's name, as defined at the Java method.
    *           
    * @return   See above.
    */
   private LegacyServiceParameter buildJavaParameter(LegacyServiceParameter orig, Type type, String name)
   {
      return new LegacyServiceParameter()
      {
         @Override
         public Class<? extends Annotation> annotationType()
         {
            return LegacyServiceParameter.class;
         }
         
         @Override
         public String type()
         {
            // 'getTypeName' will return the generic/parametrized type, as in:
            // some.pkg.Foo<SomeType1, SomeType2, ...>
            return type.getTypeName();
         }
         
         @Override
         public String target()
         {
            return orig.target();
         }
         
         @Override
         public String source()
         {
            return orig.source();
         }
         
         @Override
         public boolean returnValue()
         {
            return orig.returnValue();
         }
         
         @Override
         public boolean output()
         {
            return orig.output();
         }
         
         @Override
         public int ordinal()
         {
            return orig.ordinal();
         }
         
         @Override
         public String name()
         {
            return name;
         }
         
         @Override
         public boolean input()
         {
            return orig.input();
         }
         
         @Override
         public int extent()
         {
            return orig.extent();
         }
         
         @Override
         public boolean allowUnknown()
         {
            return orig.allowUnknown();
         }
         
         @Override
         public String defaultValue()
         {
            return orig.defaultValue();
         }
      };   
   }
}