RestService.java
/*
** Module : RestService.java
** Abstract : Details of a REST service specification.
**
** Copyright (c) 2019-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA 20190614 First version.
** 002 CA 20190628 Proper SINGLETON and SINGLE-RUN implementation; misc improvements.
** 003 CA 20200514 Refactoring to allow common code for SOAP services support.
** 004 CA 20210226 Pass the argument modes to the invocation, as the Agent will not pre-process the args
** otherwise.
** CA 20211112 Fixed some problems resetting the last invoke configuration.
** CA 20220120 Mutable state must be thread-local.
** 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.
** 005 SBI 20230216 Fixed buildDefinitionParameters() because defaultValue() was added to LegacyServiceParameter.
*/
/*
** 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.lang.annotation.*;
import java.util.*;
import java.util.function.*;
import com.goldencode.p2j.util.*;
/**
* Base class for an invocation implementation in a REST or SOAP request.
*/
public abstract class RestService
{
/** The REST key. */
public final ServiceKey key;
/** The annotation with the details for this REST operation. */
public final LegacyService ls;
/** The service signature. */
public final LegacySignature serviceSignature;
/** The invocation timeout. */
protected final int timeout;
/** The configuration for the last service call. */
protected ThreadLocal<InvokeConfig> cfg = new ThreadLocal<>();
/**
* The parameters as they appear at the target Java method, converted to {@link LegacyServiceParameter}.
*/
protected LegacyServiceParameter[] definitionParameters = null;
/**
* Create a new instance to handle invocations for the specified REST operation.
*
* @param ls
* The operation details.
* @param sig
* The service signature. Non-null only for legacy services.
* @param key
* The service key.
* @param timeout
* The invocation timeout.
*/
public RestService(LegacyService ls, LegacySignature sig, ServiceKey key, int timeout)
{
this.ls = ls;
this.serviceSignature = sig;
this.key = key;
this.timeout = timeout;
for (LegacyServiceParameter lsp : ls.parameters())
{
if (lsp.name().isEmpty())
{
throw new RuntimeException("The name for parameter " + lsp.ordinal() + " at service " +
ls.name() + " must be provided");
}
else if (lsp.type().isEmpty())
{
throw new RuntimeException("The type for parameter " + lsp.ordinal() + " at service " +
ls.name() + " must be provided");
}
}
buildDefinitionParameters();
}
/**
* Invoke this service.
*
* @param helper
* The appserver helper used to execute the requests.
* @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 args
* The arguments.
*
* @return The returned value.
*/
public abstract Object invoke(AppServerHelper helper, String token, Object[] args);
/**
* Check if the last call of this service was for running a persistent procedure.
*
* @return See above.
*/
public boolean isPersistent()
{
InvokeConfig cfg = this.cfg.get();
return cfg != null && cfg.isPersistent();
}
/**
* Get the request ID for the last call.
*
* @return See above.
*/
public String getRequestID()
{
InvokeConfig cfg = this.cfg.get();
// if cfg is not set (i.e. argument parsing fails), then build a request ID)
return cfg == null ? nextRequestId() : cfg.getRequestId();
}
/**
* Get the persistent procedure handle set by the last call.
*
* @return See above.
*/
public handle getProcedureHandle()
{
InvokeConfig cfg = this.cfg.get();
return cfg == null ? null : cfg.getProcedureHandle();
}
/**
* Get the {@link #definitionParameters}, as they appear at the Java method's definition.
* <pre>
* These are built via {@link #buildDefinitionParameters()}, depending on service's type. For legacy REST
* services they are built using the signature's {@link LegacyParameter} annotation, while for Java REST
* services, they are built using the Java method's signature.
*
* @return See above.
*/
public LegacyServiceParameter[] getDefinitionParameters()
{
return definitionParameters;
}
/**
* Get the execution mode for this service.
*
* @return See above.
*/
public String getExecutionMode()
{
return ls.executionMode();
}
/**
* Reset the last invoke {@link #cfg configuration}.
*/
public void reset()
{
cfg.set(null);
}
/**
* Compute a request ID, a 22 byte string with ':' delimiters after the 7th, 11th and 17th byte.
*
* @return See above.
*/
protected String nextRequestId()
{
Random r = new Random();
//
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// 25f594c9579abdba:017f9ab5:016d6c6ca0d0:-7fff#02fa
byte[] bytes = new byte[20];
r.nextBytes(bytes);
String s = "";
for (int i = 0; i < bytes.length; i++)
{
int k = bytes[i] & 0xFF;
String hex = Integer.toHexString(k);
while (hex.length() < 2)
{
hex = "0" + hex;
}
s = s + hex + (i == 7 || i == 11 || i == 17 ? ":" : "");
}
s = s + "#" + Integer.toHexString(r.nextInt(65535));
return s;
}
/**
* Resolve the parameter modes from the {@link #serviceSignature signature}.
*/
protected String resolveModes()
{
String modes = null;
for (LegacyParameter lp : serviceSignature.parameters())
{
if (modes == null)
{
modes = "";
}
switch (lp.mode())
{
case "INPUT":
modes = modes + "I";
break;
case "OUTPUT":
modes = modes + "O";
break;
case "INPUT-OUTPUT":
modes = modes + "U";
break;
}
}
return modes;
}
/**
* Get the parser to be used for parsing the arguments, to be attached to the service's parser.
*
* @param argumentsParser
* The service's parser.
*
* @return Always a new {@link LegacyArgumentsParser} instance.
*/
protected ArgumentsParser getParser(ServiceArgumentsParser argumentsParser)
{
return new LegacyArgumentsParser(argumentsParser);
}
/**
* Get the serialized to be used for writing the response arguments.
*
* @param argumentsSerializer
* The service's serializer.
*
* @return Always a {@link LegacyArgumentsSerializer} instance.
*/
protected ArgumentsSerializer getSerializer(ResponseArguments argumentsSerializer)
{
return new LegacyArgumentsSerializer(argumentsSerializer);
}
/**
* Build the parameters, from the target method's {@link LegacyParameter} annotations. This will set the
* {@link #definitionParameters} array.
*/
protected void buildDefinitionParameters()
{
BiFunction<LegacyParameter, Integer, LegacyServiceParameter> create = (lp, ordinal) ->
new LegacyServiceParameter()
{
@Override
public Class<? extends Annotation> annotationType()
{
return LegacyServiceParameter.class;
}
@Override
public String type()
{
return lp.type();
}
@Override
public int ordinal()
{
return ordinal;
}
@Override
public String name()
{
return lp.name();
}
@Override
public boolean output()
{
return lp.mode().equals("OUTPUT") || lp.mode().equals("INPUT-OUTPUT");
}
@Override
public boolean input()
{
return lp.mode().equals("INPUT") || lp.mode().equals("INPUT-OUTPUT");
}
@Override
public int extent()
{
return lp.extent();
}
public String target() { return null; }
public String source() { return null; }
public boolean returnValue() { return false; }
public boolean allowUnknown() { return false; }
@Override
public String defaultValue()
{
return "";
}
};
int size = serviceSignature.parameters().length;
LegacyServiceParameter[] params = new LegacyServiceParameter[size];
for (int i = 0; i < params.length; i++)
{
LegacyParameter lp = serviceSignature.parameters()[i];
int ordinal = i + 1;
params[i] = create.apply(lp, ordinal);
}
definitionParameters = params;
}
}