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();
}
};
}
}