add haskell generator

This commit is contained in:
wing328 2018-03-26 22:39:27 +08:00
parent 246ed57547
commit 7fbc8fa31e
7 changed files with 671 additions and 6 deletions

View File

@ -1097,6 +1097,7 @@ public class DefaultCodegen implements CodegenConfig {
**/
@SuppressWarnings("static-method")
public String getSchemaType(Schema schema) {
// datatype is the OAI type (e.g. integer, long, etc)
String datatype = null;
if (StringUtils.isNotBlank(schema.get$ref())) { // object
@ -1141,7 +1142,7 @@ public class DefaultCodegen implements CodegenConfig {
if (SchemaTypeUtil.INTEGER64_FORMAT.equals(schema.getFormat())) {
datatype = "long";
} else {
datatype = schema.getType();
datatype = schema.getType(); // integer
}
} else if (schema instanceof MapSchema) {
datatype = "map";
@ -1565,7 +1566,6 @@ public class DefaultCodegen implements CodegenConfig {
}
String type = getSchemaType(p);
//LOGGER.info("from property = " + p);
if (p instanceof IntegerSchema || SchemaTypeUtil.INTEGER_TYPE.equals(p.getType())) {
property.isNumeric = Boolean.TRUE;
if (SchemaTypeUtil.INTEGER64_FORMAT.equals(p.getFormat())) {
@ -1718,6 +1718,7 @@ public class DefaultCodegen implements CodegenConfig {
property.allowableValues = allowableValues;
}
}
property.datatype = getTypeDeclaration(p);
property.dataFormat = p.getFormat();
@ -1978,7 +1979,7 @@ public class DefaultCodegen implements CodegenConfig {
Operation operation,
Map<String, Schema> schemas,
OpenAPI openAPI) {
//LOGGER.info("fromOperation => operation: " + operation);
LOGGER.debug("fromOperation => operation: " + operation);
CodegenOperation op = CodegenModelFactory.newInstance(CodegenModelType.OPERATION);
Set<String> imports = new HashSet<String>();
if (operation.getExtensions() != null && !operation.getExtensions().isEmpty()) {
@ -2191,9 +2192,7 @@ public class DefaultCodegen implements CodegenConfig {
// add imports to operation import tag
for (String i : imports) {
LOGGER.info("debugging fromOperation imports: " + i);
if (needToImport(i)) {
LOGGER.info("debugging fromOperation imports: " + i + " imported");
op.imports.add(i);
}
}
@ -2671,6 +2670,7 @@ public class DefaultCodegen implements CodegenConfig {
setParameterExampleValue(codegenParameter);
postProcessParameter(codegenParameter);
LOGGER.info("debugging codegenParameter return: " + codegenParameter);
return codegenParameter;
}

View File

@ -0,0 +1,563 @@
package org.openapitools.codegen.languages;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.SupportingFile;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.media.*;
import java.util.*;
import java.util.regex.Pattern;
public class HaskellServantCodegen extends DefaultCodegen implements CodegenConfig {
// source folder where to write the files
protected String sourceFolder = "src";
protected String apiVersion = "0.0.1";
private static final Pattern LEADING_UNDERSCORE = Pattern.compile("^_+");
/**
* Configures the type of generator.
*
* @return the CodegenType for this generator
*/
public CodegenType getTag() {
return CodegenType.SERVER;
}
/**
* Configures a friendly name for the generator. This will be used by the generator
* to select the library with the -l flag.
*
* @return the friendly name for the generator
*/
public String getName() {
return "haskell";
}
/**
* Returns human-friendly help for the generator. Provide the consumer with help
* tips, parameters here
*
* @return A string value for the help message
*/
public String getHelp() {
return "Generates a Haskell server and client library.";
}
public HaskellServantCodegen() {
super();
// override the mapping to keep the original mapping in Haskell
specialCharReplacements.put("-", "Dash");
specialCharReplacements.put(">", "GreaterThan");
specialCharReplacements.put("<", "LessThan");
// backslash and double quote need double the escapement for both Java and Haskell
specialCharReplacements.remove("\\");
specialCharReplacements.remove("\"");
specialCharReplacements.put("\\\\", "Back_Slash");
specialCharReplacements.put("\\\"", "Double_Quote");
// set the output folder here
outputFolder = "generated-code/haskell-servant";
/*
* Template Location. This is the location which templates will be read from. The generator
* will use the resource stream to attempt to read the templates.
*/
embeddedTemplateDir = templateDir = "haskell-servant";
/*
* Api Package. Optional, if needed, this can be used in templates
*/
apiPackage = "API";
/*
* Model Package. Optional, if needed, this can be used in templates
*/
modelPackage = "Types";
// Haskell keywords and reserved function names, taken mostly from https://wiki.haskell.org/Keywords
setReservedWordsLowerCase(
Arrays.asList(
// Keywords
"as", "case", "of",
"class", "data", "family",
"default", "deriving",
"do", "forall", "foreign", "hiding",
"if", "then", "else",
"import", "infix", "infixl", "infixr",
"instance", "let", "in",
"mdo", "module", "newtype",
"proc", "qualified", "rec",
"type", "where"
)
);
/*
* Additional Properties. These values can be passed to the templates and
* are available in models, apis, and supporting files
*/
additionalProperties.put("apiVersion", apiVersion);
/*
* Supporting Files. You can write single files for the generator with the
* entire object tree available. If the input file has a suffix of `.mustache
* it will be processed by the template engine. Otherwise, it will be copied
*/
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
supportingFiles.add(new SupportingFile("stack.mustache", "", "stack.yaml"));
supportingFiles.add(new SupportingFile("Setup.mustache", "", "Setup.hs"));
/*
* Language Specific Primitives. These types will not trigger imports by
* the client generator
*/
languageSpecificPrimitives = new HashSet<String>(
Arrays.asList(
"Bool",
"String",
"Int",
"Integer",
"Float",
"Char",
"Double",
"List",
"FilePath"
)
);
typeMapping.clear();
typeMapping.put("array", "List");
typeMapping.put("set", "Set");
typeMapping.put("boolean", "Bool");
typeMapping.put("string", "Text");
typeMapping.put("integer", "Int");
typeMapping.put("long", "Integer");
typeMapping.put("short", "Int");
typeMapping.put("char", "Char");
typeMapping.put("float", "Float");
typeMapping.put("double", "Double");
typeMapping.put("DateTime", "Integer");
typeMapping.put("file", "FilePath");
typeMapping.put("number", "Double");
typeMapping.put("any", "Value");
typeMapping.put("UUID", "Text");
typeMapping.put("ByteArray", "Text");
typeMapping.put("object", "Value");
importMapping.clear();
importMapping.put("Map", "qualified Data.Map as Map");
cliOptions.add(new CliOption(CodegenConstants.MODEL_PACKAGE, CodegenConstants.MODEL_PACKAGE_DESC));
cliOptions.add(new CliOption(CodegenConstants.API_PACKAGE, CodegenConstants.API_PACKAGE_DESC));
}
/**
* Escapes a reserved word as defined in the `reservedWords` array. Handle escaping
* those terms here. This logic is only called if a variable matches the reserved words
*
* @return the escaped term
*/
@Override
public String escapeReservedWord(String name) {
if(this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
}
public String firstLetterToUpper(String word) {
if (word.length() == 0) {
return word;
} else if (word.length() == 1) {
return word.substring(0, 1).toUpperCase();
} else {
return word.substring(0, 1).toUpperCase() + word.substring(1);
}
}
public String firstLetterToLower(String word) {
if (word.length() == 0) {
return word;
} else if (word.length() == 1) {
return word.substring(0, 1).toLowerCase();
} else {
return word.substring(0, 1).toLowerCase() + word.substring(1);
}
}
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
// From the title, compute a reasonable name for the package and the API
String title = openAPI.getInfo().getTitle();
// Drop any API suffix
if(title == null) {
title = "OpenAPI";
} else {
title = title.trim();
if (title.toUpperCase().endsWith("API")) {
title = title.substring(0, title.length() - 3);
}
}
String[] words = title.split(" ");
// The package name is made by appending the lowercased words of the title interspersed with dashes
List<String> wordsLower = new ArrayList<String>();
for (String word : words) {
wordsLower.add(word.toLowerCase());
}
String cabalName = joinStrings("-", wordsLower);
// The API name is made by appending the capitalized words of the title
List<String> wordsCaps = new ArrayList<String>();
for (String word : words) {
wordsCaps.add(firstLetterToUpper(word));
}
String apiName = joinStrings("", wordsCaps);
// Set the filenames to write for the API
supportingFiles.add(new SupportingFile("haskell-servant-codegen.mustache", "", cabalName + ".cabal"));
supportingFiles.add(new SupportingFile("API.mustache", "lib/" + apiName, "API.hs"));
supportingFiles.add(new SupportingFile("Types.mustache", "lib/" + apiName, "Types.hs"));
additionalProperties.put("title", apiName);
additionalProperties.put("titleLower", firstLetterToLower(apiName));
additionalProperties.put("package", cabalName);
// Due to the way servant resolves types, we need a high context stack limit
additionalProperties.put("contextStackLimit", openAPI.getPaths().size() * 2 + 300);
List<Map<String, Object>> replacements = new ArrayList<>();
Object[] replacementChars = specialCharReplacements.keySet().toArray();
for(int i = 0; i < replacementChars.length; i++) {
String c = (String) replacementChars[i];
Map<String, Object> o = new HashMap<>();
o.put("char", c);
o.put("replacement", "'" + specialCharReplacements.get(c));
o.put("hasMore", i != replacementChars.length - 1);
replacements.add(o);
}
additionalProperties.put("specialCharReplacements", replacements);
super.preprocessOpenAPI(openAPI);
}
/**
* Optional - type declaration. This is a String which is used by the templates to instantiate your
* types. There is typically special handling for different property types
*
* @return a string value used as the `dataType` field for model templates, `returnType` for api templates
*/
@Override
public String getTypeDeclaration(Schema p) {
if (p instanceof ArraySchema) {
ArraySchema ap = (ArraySchema) p;
Schema inner = ap.getItems();
return "[" + getTypeDeclaration(inner) + "]";
} else if (p instanceof MapSchema) {
MapSchema mp = (MapSchema) p;
Schema inner = (Schema) mp.getAdditionalProperties();
return "Map.Map String " + getTypeDeclaration(inner);
}
return fixModelChars(super.getTypeDeclaration(p));
}
/**
* Optional - OpenAPI type conversion. This is used to map OpenAPI types in a `Schema` into
* either language specific types via `typeMapping` or into complex models if there is not a mapping.
*
* @return a string value of the type or complex model for this property
*/
@Override
public String getSchemaType(Schema p) {
String swaggerType = super.getSchemaType(p);
LOGGER.debug("debugging swager type: " + p.getType() + ", " + p.getFormat() + " => " + swaggerType);
String type = null;
if (typeMapping.containsKey(swaggerType)) {
type = typeMapping.get(swaggerType);
return type;
//if (languageSpecificPrimitives.contains(type))
// return toModelName(type);
} else if(typeMapping.containsValue(swaggerType)) {
// TODO what's this case for?
type = swaggerType + "_";
} else {
type = swaggerType;
}
// it's a model
return toModelName(type);
}
@Override
public String toInstantiationType(Schema p) {
if (p instanceof MapSchema) {
MapSchema ap = (MapSchema) p;
Schema additionalProperties2 = (Schema) ap.getAdditionalProperties();
String type = additionalProperties2.getType();
if (null == type) {
LOGGER.error("No Type defined for Additional Property " + additionalProperties2 + "\n" //
+ "\tIn Property: " + p);
}
String inner = getSchemaType(additionalProperties2);
return "(Map.Map Text " + inner + ")";
} else if (p instanceof ArraySchema) {
ArraySchema ap = (ArraySchema) p;
String inner = getSchemaType(ap.getItems());
// Return only the inner type; the wrapping with QueryList is done
// somewhere else, where we have access to the collection format.
return inner;
} else {
return null;
}
}
// Intersperse a separator string between a list of strings, like String.join.
private String joinStrings(String sep, List<String> ss) {
StringBuilder sb = new StringBuilder();
for (String s : ss) {
if (sb.length() > 0) {
sb.append(sep);
}
sb.append(s);
}
return sb.toString();
}
// Convert an HTTP path to a Servant route, including captured parameters.
// For example, the path /api/jobs/info/{id}/last would become:
// "api" :> "jobs" :> "info" :> Capture "id" IdType :> "last"
// IdType is provided by the capture params.
private List<String> pathToServantRoute(String path, List<CodegenParameter> pathParams) {
// Map the capture params by their names.
HashMap<String, String> captureTypes = new HashMap<String, String>();
for (CodegenParameter param : pathParams) {
captureTypes.put(param.baseName, param.dataType);
}
// Cut off the leading slash, if it is present.
if (path.startsWith("/")) {
path = path.substring(1);
}
// Convert the path into a list of servant route components.
List<String> pathComponents = new ArrayList<String>();
for (String piece : path.split("/")) {
if (piece.startsWith("{") && piece.endsWith("}")) {
String name = piece.substring(1, piece.length() - 1);
pathComponents.add("Capture \"" + name + "\" " + captureTypes.get(name));
} else {
pathComponents.add("\"" + piece + "\"");
}
}
// Intersperse the servant route pieces with :> to construct the final API type
return pathComponents;
}
// Extract the arguments that are passed in the route path parameters
private List<String> pathToClientType(String path, List<CodegenParameter> pathParams) {
// Map the capture params by their names.
HashMap<String, String> captureTypes = new HashMap<String, String>();
for (CodegenParameter param : pathParams) {
captureTypes.put(param.baseName, param.dataType);
}
// Cut off the leading slash, if it is present.
if (path.startsWith("/")) {
path = path.substring(1);
}
// Convert the path into a list of servant route components.
List<String> type = new ArrayList<String>();
for (String piece : path.split("/")) {
if (piece.startsWith("{") && piece.endsWith("}")) {
String name = piece.substring(1, piece.length() - 1);
type.add(captureTypes.get(name));
}
}
return type;
}
@Override
public CodegenOperation fromOperation(String resourcePath, String httpMethod, Operation operation, Map<String, Schema> definitions, OpenAPI openAPI) {
CodegenOperation op = super.fromOperation(resourcePath, httpMethod, operation, definitions, openAPI);
List<String> path = pathToServantRoute(op.path, op.pathParams);
List<String> type = pathToClientType(op.path, op.pathParams);
// Query parameters appended to routes
for (CodegenParameter param : op.queryParams) {
String paramType = param.dataType;
if (param.isListContainer) {
if (StringUtils.isEmpty(param.collectionFormat)) {
param.collectionFormat = "csv";
}
paramType = makeQueryListType(paramType, param.collectionFormat);
}
path.add("QueryParam \"" + param.baseName + "\" " + paramType);
type.add("Maybe " + param.dataType);
}
// Either body or form data parameters appended to route
// As far as I know, you cannot have two ReqBody routes.
// Is it possible to have body params AND have form params?
String bodyType = null;
if (op.getHasBodyParam()) {
for (CodegenParameter param : op.bodyParams) {
path.add("ReqBody '[JSON] " + param.dataType);
bodyType = param.dataType;
}
} else if(op.getHasFormParams()) {
// Use the FormX data type, where X is the conglomerate of all things being passed
String formName = "Form" + camelize(op.operationId);
bodyType = formName;
path.add("ReqBody '[FormUrlEncoded] " + formName);
}
if (bodyType != null) {
type.add(bodyType);
}
// Special headers appended to route
for (CodegenParameter param : op.headerParams) {
path.add("Header \"" + param.baseName + "\" " + param.dataType);
String paramType = param.dataType;
if (param.isListContainer) {
if (StringUtils.isEmpty(param.collectionFormat)) {
param.collectionFormat = "csv";
}
paramType = makeQueryListType(paramType, param.collectionFormat);
}
type.add("Maybe " + paramType);
}
// store form parameter name in the vendor extensions
for (CodegenParameter param : op.formParams) {
param.vendorExtensions.put("x-formParamName", camelize(param.baseName));
}
// Add the HTTP method and return type
String returnType = op.returnType;
if (returnType == null || returnType.equals("null")) {
returnType = "()";
}
if (returnType.indexOf(" ") >= 0) {
returnType = "(" + returnType + ")";
}
path.add("Verb '" + op.httpMethod.toUpperCase() + " 200 '[JSON] " + returnType);
type.add("m " + returnType);
op.vendorExtensions.put("x-routeType", joinStrings(" :> ", path));
op.vendorExtensions.put("x-clientType", joinStrings(" -> ", type));
op.vendorExtensions.put("x-formName", "Form" + camelize(op.operationId));
for(CodegenParameter param : op.formParams) {
param.vendorExtensions.put("x-formPrefix", camelize(op.operationId, true));
}
return op;
}
private String makeQueryListType(String type, String collectionFormat) {
type = type.substring(1, type.length() - 1);
switch(collectionFormat) {
case "csv": return "(QueryList 'CommaSeparated (" + type + "))";
case "tsv": return "(QueryList 'TabSeparated (" + type + "))";
case "ssv": return "(QueryList 'SpaceSeparated (" + type + "))";
case "pipes": return "(QueryList 'PipeSeparated (" + type + "))";
case "multi": return "(QueryList 'MultiParamArray (" + type + "))";
default:
throw new UnsupportedOperationException();
}
}
private String fixOperatorChars(String string) {
StringBuilder sb = new StringBuilder();
String name = string;
//Check if it is a reserved word, in which case the underscore is added when property name is generated.
if (string.startsWith("_")) {
if (reservedWords.contains(string.substring(1, string.length()))) {
name = string.substring(1, string.length());
} else if (reservedWordsMappings.containsValue(string)) {
name = LEADING_UNDERSCORE.matcher(string).replaceFirst("");
}
}
for (char c : name.toCharArray()) {
String cString = String.valueOf(c);
if (specialCharReplacements.containsKey(cString)) {
sb.append("'");
sb.append(specialCharReplacements.get(cString));
} else {
sb.append(c);
}
}
return sb.toString();
}
// Remove characters from a string that do not belong in a model classname
private String fixModelChars(String string) {
return string.replace(".", "").replace("-", "");
}
// Override fromModel to create the appropriate model namings
@Override
public CodegenModel fromModel(String name, Schema mod, Map<String, Schema> allDefinitions) {
CodegenModel model = super.fromModel(name, mod, allDefinitions);
// Clean up the class name to remove invalid characters
model.classname = fixModelChars(model.classname);
if(typeMapping.containsValue(model.classname)) {
model.classname += "_";
}
// From the model name, compute the prefix for the fields.
String prefix = camelize(model.classname, true);
for(CodegenProperty prop : model.vars) {
prop.name = toVarName(prefix + camelize(fixOperatorChars(prop.name)));
}
// Create newtypes for things with non-object types
String dataOrNewtype = "data";
if(model.dataType != "object" && typeMapping.containsKey(model.dataType)) {
String newtype = typeMapping.get(model.dataType);
model.vendorExtensions.put("x-customNewtype", newtype);
}
// Provide the prefix as a vendor extension, so that it can be used in the ToJSON and FromJSON instances.
model.vendorExtensions.put("x-prefix", prefix);
model.vendorExtensions.put("x-data", dataOrNewtype);
return model;
}
@Override
public String escapeQuotationMark(String input) {
// remove " to avoid code injection
return input.replace("\"", "");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("{-", "{_-").replace("-}", "-_}");
}
}

View File

@ -1,4 +1,5 @@
org.openapitools.codegen.languages.BashClientCodegen
org.openapitools.codegen.languages.HaskellServantCodegen
org.openapitools.codegen.languages.ObjcClientCodegen
org.openapitools.codegen.languages.PhpClientCodegen
org.openapitools.codegen.languages.RubyClientCodegen

View File

@ -0,0 +1,25 @@
package org.openapitools.codegen.haskellservant;
import io.swagger.v3.oas.models.PathItem;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.languages.HaskellServantCodegen;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.testng.Assert;
import org.testng.annotations.Test;
public class HaskellModelTest {
@Test(description = "convert a haskell model with dots")
public void modelTest() {
Assert.assertEquals(true, true);
}
}

View File

@ -0,0 +1,37 @@
package org.openapitools.codegen.haskellservant;
import org.openapitools.codegen.AbstractOptionsTest;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.languages.HaskellServantCodegen;
import org.openapitools.codegen.options.HaskellServantOptionsProvider;
import mockit.Expectations;
import mockit.Tested;
public class HaskellServantOptionsTest extends AbstractOptionsTest {
@Tested
private HaskellServantCodegen clientCodegen;
public HaskellServantOptionsTest() {
super(new HaskellServantOptionsProvider());
}
@Override
protected CodegenConfig getCodegenConfig() {
return clientCodegen;
}
@Override
protected void setExpectations() {
new Expectations(clientCodegen) {{
clientCodegen.setModelPackage(HaskellServantOptionsProvider.MODEL_PACKAGE_VALUE);
times = 1;
clientCodegen.setApiPackage(HaskellServantOptionsProvider.API_PACKAGE_VALUE);
times = 1;
clientCodegen.setSortParamsByRequiredFlag(Boolean.valueOf(HaskellServantOptionsProvider.SORT_PARAMS_VALUE));
times = 1;
}};
}
}

View File

@ -347,7 +347,7 @@ public class PhpModelTest {
}
// datetime (or primitive type) not yet supported in HTTP request body
@Test(description = "returns DateTime when using `--model-name-prefix`", enabled = false)
@Test(description = "returns DateTime when using `--model-name-prefix`", enabled = false)
public void dateTest() {
final OpenAPI model = new OpenAPIV3Parser().read("src/test/resources/2_0/datePropertyTest.json");
final DefaultCodegen codegen = new PhpClientCodegen();

View File

@ -0,0 +1,39 @@
package org.openapitools.codegen.options;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.languages.HaskellServantCodegen;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
public class HaskellServantOptionsProvider implements OptionsProvider {
public static final String MODEL_PACKAGE_VALUE = "Model";
public static final String API_PACKAGE_VALUE = "Api";
public static final String SORT_PARAMS_VALUE = "false";
public static final String ENSURE_UNIQUE_PARAMS_VALUE = "true";
public static final String ALLOW_UNICODE_IDENTIFIERS_VALUE = "false";
public static final String PREPEND_FORM_OR_BODY_PARAMETERS_VALUE = "true";
@Override
public String getLanguage() {
return "haskell";
}
@Override
public Map<String, String> createOptions() {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<String, String>();
return builder.put(CodegenConstants.MODEL_PACKAGE, MODEL_PACKAGE_VALUE)
.put(CodegenConstants.API_PACKAGE, API_PACKAGE_VALUE)
.put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, SORT_PARAMS_VALUE)
.put(CodegenConstants.ENSURE_UNIQUE_PARAMS, ENSURE_UNIQUE_PARAMS_VALUE)
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
.build();
}
@Override
public boolean isServer() {
return true;
}
}