/*
HTTP stub server written in Java with embedded Jetty
Copyright (C) 2012 Alexander Zagniotov, Isa Goksu and Eric Mrak
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.github.azagniotov.stubby4j.yaml;
import io.github.azagniotov.stubby4j.annotations.CoberturaIgnore;
import io.github.azagniotov.stubby4j.cli.ANSITerminal;
import io.github.azagniotov.stubby4j.stubs.AbstractBuilder;
import io.github.azagniotov.stubby4j.stubs.ReflectableStub;
import io.github.azagniotov.stubby4j.stubs.StubHttpLifecycle;
import io.github.azagniotov.stubby4j.stubs.StubRequest;
import io.github.azagniotov.stubby4j.stubs.StubResponse;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import static io.github.azagniotov.generics.TypeSafeConverter.asCheckedArrayList;
import static io.github.azagniotov.generics.TypeSafeConverter.asCheckedLinkedHashMap;
import static io.github.azagniotov.stubby4j.stubs.StubbableAuthorizationType.BASIC;
import static io.github.azagniotov.stubby4j.stubs.StubbableAuthorizationType.BEARER;
import static io.github.azagniotov.stubby4j.stubs.StubbableAuthorizationType.CUSTOM;
import static io.github.azagniotov.stubby4j.utils.ConsoleUtils.logUnmarshalledStubRequest;
import static io.github.azagniotov.stubby4j.utils.FileUtils.constructInputStream;
import static io.github.azagniotov.stubby4j.utils.FileUtils.isFilePathContainTemplateTokens;
import static io.github.azagniotov.stubby4j.utils.FileUtils.uriToFile;
import static io.github.azagniotov.stubby4j.utils.StringUtils.encodeBase64;
import static io.github.azagniotov.stubby4j.utils.StringUtils.objectToString;
import static io.github.azagniotov.stubby4j.utils.StringUtils.trimIfSet;
import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.FILE;
import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.METHOD;
import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.REQUEST;
import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.RESPONSE;
import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.isUnknownProperty;
import static io.github.azagniotov.stubby4j.yaml.ConfigurableYAMLProperty.ofNullableProperty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import static org.yaml.snakeyaml.DumperOptions.FlowStyle;
public class YAMLParser {
static final String FAILED_TO_LOAD_FILE_ERR = "Failed to retrieveLoadedStubs response content using relative path specified in 'file'. Check that response content exists in relative path specified in 'file'";
private final static Yaml SNAKE_YAML = SnakeYaml.INSTANCE.getSnakeYaml();
private final AtomicInteger parsedStubCounter = new AtomicInteger();
private String dataConfigHomeDirectory;
@CoberturaIgnore
public List<StubHttpLifecycle> parse(final String dataConfigHomeDirectory, final String configContent) throws IOException {
return parse(dataConfigHomeDirectory, constructInputStream(configContent));
}
@CoberturaIgnore
public List<StubHttpLifecycle> parse(final String dataConfigHomeDirectory, final File configFile) throws IOException {
return parse(dataConfigHomeDirectory, constructInputStream(configFile));
}
private List<StubHttpLifecycle> parse(final String dataConfigHomeDirectory, final InputStream configAsStream) throws IOException {
this.dataConfigHomeDirectory = dataConfigHomeDirectory;
final Object loadedConfig = SNAKE_YAML.load(configAsStream);
if (!(loadedConfig instanceof List)) {
throw new IOException("Loaded YAML root node must be an instance of ArrayList, otherwise something went wrong. Check provided YAML");
}
final List<StubHttpLifecycle> stubs = new LinkedList<>();
final List<Map> httpLifecycleConfigs = asCheckedArrayList(loadedConfig, Map.class);
for (final Map rawHttpLifecycleConfig : httpLifecycleConfigs) {
final Map<String, Object> httpLifecycleProperties = asCheckedLinkedHashMap(rawHttpLifecycleConfig, String.class, Object.class);
stubs.add(parseStubbedHttpLifecycleConfig(httpLifecycleProperties));
}
return stubs;
}
private StubHttpLifecycle parseStubbedHttpLifecycleConfig(final Map<String, Object> httpLifecycleConfig) {
final StubHttpLifecycle.Builder stubBuilder = new StubHttpLifecycle.Builder();
for (final Map.Entry<String, Object> stubType : httpLifecycleConfig.entrySet()) {
final Object stubTypeValue = stubType.getValue();
if (stubTypeValue instanceof Map) {
final Map<String, Object> stubbedProperties = asCheckedLinkedHashMap(stubTypeValue, String.class, Object.class);
if (isRequestProperty(stubType.getKey())) {
parseStubbedRequestConfig(stubBuilder, stubbedProperties);
} else {
parseStubbedResponseConfig(stubBuilder, stubbedProperties);
}
} else if (stubTypeValue instanceof List) {
parseStubbedResponseListConfig(stubBuilder, stubType);
}
}
return stubBuilder.withCompleteYAML(toCompleteYAMLString(httpLifecycleConfig))
.withRequestAsYAML(toYAMLString(httpLifecycleConfig, REQUEST))
.withResponseAsYAML(toYAMLString(httpLifecycleConfig, RESPONSE))
.withResourceId(parsedStubCounter.getAndIncrement())
.build();
}
private void parseStubbedRequestConfig(final StubHttpLifecycle.Builder stubBuilder, final Map<String, Object> requestProperties) {
final StubRequest requestStub = buildReflectableStub(requestProperties, new StubRequest.Builder());
requestStub.compileRegexPatternsAndCache();
stubBuilder.withRequest(requestStub);
logUnmarshalledStubRequest(requestStub.getMethod(), requestStub.getUrl());
}
private void parseStubbedResponseConfig(final StubHttpLifecycle.Builder stubBuilder, final Map<String, Object> responseProperties) {
final StubResponse responseStub = buildReflectableStub(responseProperties, new StubResponse.Builder());
stubBuilder.withResponse(responseStub);
}
private <T extends ReflectableStub, B extends AbstractBuilder<T>> T buildReflectableStub(final Map<String, Object> stubbedProperties, final B stubTypeBuilder) {
for (final Map.Entry<String, Object> propertyPair : stubbedProperties.entrySet()) {
final String stageableFieldName = propertyPair.getKey();
checkStubbedProperty(stageableFieldName);
final Object rawFieldName = propertyPair.getValue();
if (rawFieldName instanceof List) {
stubTypeBuilder.stage(ofNullableProperty(stageableFieldName), of(rawFieldName));
continue;
}
if (rawFieldName instanceof Map) {
final Map<String, String> rawHeaders = asCheckedLinkedHashMap(rawFieldName, String.class, String.class);
final Map<String, String> headers = configureAuthorizationHeader(rawHeaders);
stubTypeBuilder.stage(ofNullableProperty(stageableFieldName), of(headers));
continue;
}
if (isMethodProperty(stageableFieldName)) {
final ArrayList<String> methods = new ArrayList<>(Collections.singletonList(objectToString(rawFieldName)));
stubTypeBuilder.stage(ofNullableProperty(stageableFieldName), of(methods));
continue;
}
if (isFileProperty(stageableFieldName)) {
final Optional<Object> fileContentOptional = loadFileContentFromFileUrl(rawFieldName);
stubTypeBuilder.stage(ofNullableProperty(stageableFieldName), fileContentOptional);
continue;
}
stubTypeBuilder.stage(ofNullableProperty(stageableFieldName), ofNullable(objectToString(rawFieldName)));
}
return stubTypeBuilder.build();
}
private void parseStubbedResponseListConfig(final StubHttpLifecycle.Builder stubBuilder, final Map.Entry<String, Object> httpTypeConfig) {
final List<Map> responseProperties = asCheckedArrayList(httpTypeConfig.getValue(), Map.class);
stubBuilder.withResponse(buildStubResponseList(responseProperties, new StubResponse.Builder()));
}
private List<StubResponse> buildStubResponseList(final List<Map> responseProperties, final StubResponse.Builder stubResponseBuilder) {
final List<StubResponse> stubResponses = new LinkedList<>();
for (final Map rawPropertyPairs : responseProperties) {
final Map<String, Object> propertyPairs = asCheckedLinkedHashMap(rawPropertyPairs, String.class, Object.class);
for (final Map.Entry<String, Object> propertyPair : propertyPairs.entrySet()) {
final String stageableFieldName = propertyPair.getKey();
checkStubbedProperty(stageableFieldName);
if (isFileProperty(stageableFieldName)) {
final Optional<Object> fileContentOptional = loadFileContentFromFileUrl(propertyPair.getValue());
stubResponseBuilder.stage(ofNullableProperty(stageableFieldName), fileContentOptional);
} else {
stubResponseBuilder.stage(ofNullableProperty(stageableFieldName), ofNullable(propertyPair.getValue()));
}
}
stubResponses.add(stubResponseBuilder.build());
}
return stubResponses;
}
private boolean isRequestProperty(final String stubbedProperty) {
return stubbedProperty.toLowerCase().equals(REQUEST.toString());
}
private boolean isMethodProperty(final String stubbedProperty) {
return stubbedProperty.toLowerCase().equals(METHOD.toString());
}
private boolean isFileProperty(final String stubbedProperty) {
return stubbedProperty.toLowerCase().equals(FILE.toString());
}
private Optional<Object> loadFileContentFromFileUrl(final Object configPropertyNamedFile) {
final String filePath = objectToString(configPropertyNamedFile);
try {
if (isFilePathContainTemplateTokens(new File(filePath))) {
return of(new File(dataConfigHomeDirectory, filePath));
}
return ofNullable(uriToFile(dataConfigHomeDirectory, filePath));
} catch (final IOException ex) {
ANSITerminal.error(ex.getMessage() + " " + FAILED_TO_LOAD_FILE_ERR);
}
return Optional.empty();
}
private String toCompleteYAMLString(final Map<String, Object> httpLifecycleConfig) {
final List<Map<String, Object>> root = new ArrayList<Map<String, Object>>() {{
add(httpLifecycleConfig);
}};
return SNAKE_YAML.dumpAs(root, null, FlowStyle.BLOCK);
}
private String toYAMLString(final Map<String, Object> httpLifecycleConfig, final ConfigurableYAMLProperty stubName) {
final Map<String, Object> httpType = new HashMap<String, Object>() {{
put(stubName.toString(), httpLifecycleConfig.get(stubName.toString()));
}};
return SNAKE_YAML.dumpAs(httpType, null, FlowStyle.BLOCK);
}
private Map<String, String> configureAuthorizationHeader(final Map<String, String> rawHeaders) {
final Map<String, String> headers = new LinkedHashMap<>();
for (final Map.Entry<String, String> entry : rawHeaders.entrySet()) {
headers.put(entry.getKey(), entry.getValue());
if (headers.containsKey(BASIC.asYAMLProp())) {
final String headerValue = headers.get(BASIC.asYAMLProp());
final String authorizationHeader = trimIfSet(headerValue);
final String encodedAuthorizationHeader = String.format("%s %s", BASIC.asString(), encodeBase64(authorizationHeader));
headers.put(BASIC.asYAMLProp(), encodedAuthorizationHeader);
} else if (headers.containsKey(BEARER.asYAMLProp())) {
final String headerValue = headers.get(BEARER.asYAMLProp());
headers.put(BEARER.asYAMLProp(), String.format("%s %s", BEARER.asString(), trimIfSet(headerValue)));
} else if (headers.containsKey(CUSTOM.asYAMLProp())) {
final String headerValue = headers.get(CUSTOM.asYAMLProp());
headers.put(CUSTOM.asYAMLProp(), trimIfSet(headerValue));
}
}
return headers;
}
private void checkStubbedProperty(String stageableFieldName) {
if (isUnknownProperty(stageableFieldName)) {
throw new IllegalStateException("An unknown property configured: " + stageableFieldName);
}
}
}