Skip to content

Commit

Permalink
MODOAIPMH-131: Move request validation and configuration settings log…
Browse files Browse the repository at this point in the history
…ic from edge to mod-oai-pmh (#47)
  • Loading branch information
IlliaDaliek authored May 28, 2020
1 parent 6c0c64c commit d6c84cd
Show file tree
Hide file tree
Showing 14 changed files with 88 additions and 1,018 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
.settings
.vertx
/bin/
/.idea/
*.iml
5 changes: 1 addition & 4 deletions src/main/java/org/folio/edge/oaipmh/MainVerticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

import org.folio.edge.core.EdgeVerticle2;
import org.folio.edge.oaipmh.clients.aoipmh.OaiPmhOkapiClientFactory;
import org.folio.edge.oaipmh.clients.modconfiguration.ConfigurationService;
import org.folio.edge.oaipmh.clients.modconfiguration.impl.ModConfigurationService;

import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.Router;
Expand All @@ -19,8 +17,7 @@ public Router defineRoutes() {
String okapiURL = config().getString(SYS_OKAPI_URL);
int reqTimeoutMs = config().getInteger(SYS_REQUEST_TIMEOUT_MS);
OaiPmhOkapiClientFactory ocf = new OaiPmhOkapiClientFactory(vertx, okapiURL, reqTimeoutMs);
ConfigurationService configurationService = new ModConfigurationService();
OaiPmhHandler oaiPmhHandler = new OaiPmhHandler(secureStore, ocf, configurationService);
OaiPmhHandler oaiPmhHandler = new OaiPmhHandler(secureStore, ocf);

Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
Expand Down
183 changes: 15 additions & 168 deletions src/main/java/org/folio/edge/oaipmh/OaiPmhHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,25 @@

import static com.google.common.collect.ImmutableSet.of;
import static io.vertx.core.http.HttpHeaders.ACCEPT;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
import static org.apache.http.HttpStatus.SC_NOT_FOUND;
import static org.apache.http.HttpStatus.SC_OK;
import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
import static org.apache.http.HttpStatus.SC_UNPROCESSABLE_ENTITY;
import static org.folio.edge.core.Constants.TEXT_XML;
import static org.folio.edge.oaipmh.utils.Constants.FROM;
import static org.folio.edge.oaipmh.utils.Constants.IDENTIFIER;
import static org.folio.edge.oaipmh.utils.Constants.METADATA_PREFIX;
import static org.folio.edge.oaipmh.utils.Constants.RESUMPTION_TOKEN;
import static org.folio.edge.oaipmh.utils.Constants.SET;
import static org.folio.edge.oaipmh.utils.Constants.UNTIL;
import static org.folio.edge.oaipmh.utils.Constants.VERB;
import static org.openarchives.oai._2.OAIPMHerrorcodeType.BAD_ARGUMENT;
import static org.openarchives.oai._2.OAIPMHerrorcodeType.BAD_VERB;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.folio.edge.core.Handler;
import org.folio.edge.core.model.ClientInfo;
import org.folio.edge.core.security.SecureStore;
import org.folio.edge.core.utils.ApiKeyUtils;
import org.folio.edge.core.utils.OkapiClient;
import org.folio.edge.core.utils.OkapiClientFactory;
import org.folio.edge.oaipmh.clients.aoipmh.OaiPmhOkapiClient;
import org.folio.edge.oaipmh.clients.modconfiguration.ConfigurationService;
import org.folio.edge.oaipmh.domain.Verb;
import org.folio.edge.oaipmh.utils.ResponseHelper;
import org.openarchives.oai._2.OAIPMH;
import org.openarchives.oai._2.OAIPMHerrorType;
import org.openarchives.oai._2.OAIPMHerrorcodeType;
import org.openarchives.oai._2.RequestType;
import org.openarchives.oai._2.VerbType;

import io.vertx.core.MultiMap;
import com.google.common.collect.Iterables;

import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerRequest;
Expand All @@ -56,68 +31,34 @@
@Slf4j
public class OaiPmhHandler extends Handler {

private final ConfigurationService configurationService;

/** Expected valid http status codes to be returned by repository logic */
private static final Set<Integer> EXPECTED_CODES = of(SC_OK, SC_BAD_REQUEST, SC_NOT_FOUND, SC_UNPROCESSABLE_ENTITY);
private static final String ERRORS_PROCESSING_KEY = "responsestatus200";
private static final Set<Integer> EXPECTED_CODES = of(SC_OK, SC_BAD_REQUEST, SC_NOT_FOUND, SC_UNPROCESSABLE_ENTITY, SC_SERVICE_UNAVAILABLE);

public OaiPmhHandler(SecureStore secureStore, OkapiClientFactory ocf, ConfigurationService configurationService) {
public OaiPmhHandler(SecureStore secureStore, OkapiClientFactory ocf) {
super(secureStore, ocf);
this.configurationService = configurationService;
}

protected void handle(RoutingContext ctx) {
HttpServerRequest request = ctx.request();
log.debug("Client request: {} {}", request.method(), request.absoluteURI());
log.debug("Client request parameters: " + request.params());
log.debug("Client request headers: " + request.headers());
log.debug("Client request headers: " + Iterables.toString(request.headers()));

if (!supportedAcceptHeaders(request)) {
notAcceptableResponse(ctx, request);
return;
}

getOkapiClient(ctx, (okapiClient, param) -> configurationService.getEnableOaiServiceConfigSetting(okapiClient)
.setHandler(oaiPmhEnabled -> {
if (!oaiPmhEnabled.result()) {
serviceUnavailableResponse(ctx);
return;
}
configurationService.associateErrorsWith200Status(okapiClient)
.setHandler(responseStatusShouldBe200 -> {
ctx.put(ERRORS_PROCESSING_KEY, responseStatusShouldBe200.result());
Verb verb = Verb.fromName(request.getParam(VERB));
if (verb == null) {
badRequest(ctx, String.format("Bad verb. Verb '%s' is not implemented", request.getParam(VERB)), null, BAD_VERB);
return;
}
List<OAIPMHerrorType> errors = verb.validate(ctx);
if (!errors.isEmpty()) {
badRequest(ctx, verb.toString(), errors);
return;
}
String[] requiredParams;
if (verb.getExclusiveParam() != null && request.getParam(verb.getExclusiveParam()) != null) {
requiredParams = new String[] { verb.getExclusiveParam() };
} else {
requiredParams = verb.getRequiredParams()
.toArray(new String[0]);
}
super.handleCommon(ctx, requiredParams, verb.getOptionalParams()
.toArray(new String[0]), (client, params) -> {
final OaiPmhOkapiClient oaiPmhClient = new OaiPmhOkapiClient(client);
oaiPmhClient.call(request.params(), request.headers(), response -> handleProxyResponse(ctx, response),
t -> oaiPmhFailureHandler(ctx, t));
});
});
}));
handleCommon(ctx, new String[0], new String[0] , (okapiClient, params) -> {
final OaiPmhOkapiClient oaiPmhClient = new OaiPmhOkapiClient(okapiClient);
oaiPmhClient.call(request.params(), request.headers(), response -> handleProxyResponse(ctx, response), t -> oaiPmhFailureHandler(ctx, t));
});
}

/**
* EDGE-OAI-PMH supports only text/xml and all its derivatives in Accept header.
* Empty Accept header implies any MIME type is accepted, same as Accept: *//*
*
*
* Valid examples: text/xml, text/*, *//*, *\xml
* NOT Valid examples: application/xml, application/*, test/json
*
Expand Down Expand Up @@ -148,8 +89,7 @@ private boolean supportedAcceptHeaders(HttpServerRequest request) {
protected void handleProxyResponse(RoutingContext ctx, HttpClientResponse response) {
HttpServerResponse edgeResponse = ctx.response();
int httpStatusCode = response.statusCode();
ctx.response()
.setStatusCode(getErrorsProcessingConfigSetting(ctx) ? SC_OK : response.statusCode());
ctx.response().setStatusCode(response.statusCode());
if (EXPECTED_CODES.contains(httpStatusCode)) {
edgeResponse.putHeader(HttpHeaders.CONTENT_TYPE, TEXT_XML);
// In case the repository logic already compressed the response, lets transfer header to avoid potential doubled compression
Expand All @@ -163,34 +103,16 @@ protected void handleProxyResponse(RoutingContext ctx, HttpClientResponse respon
response.bodyHandler(buffer -> {
edgeResponse.end(buffer);
if (!encodingHeader.isPresent()) {
log.debug("Response from oai-pmh response:{} \n {}",response.headers(), buffer);
log.debug("Response from oai-pmh headers:{} \n {}", Iterables.toString(response.headers()), buffer);
}
log.debug("Edge response headers: {}", edgeResponse.headers());
log.debug("Edge response headers: {}", Iterables.toString(edgeResponse.headers()));
});
} else {
log.error(String.format("Error in the response from repository: (%d)", httpStatusCode));
log.error(String.format("Error in the response from repository: (%d)", response.statusCode()));
internalServerError(ctx, "Internal Server Error");
}
}

@Override
protected void badRequest(RoutingContext ctx, String body) {
String verb = ctx.request()
.getParam(VERB);
badRequest(ctx, body, verb, BAD_ARGUMENT);
}

private void badRequest(RoutingContext ctx, String body, String verb, OAIPMHerrorcodeType type) {
OAIPMH resp = buildBaseResponse(ctx, verb).withErrors(new OAIPMHerrorType().withCode(type)
.withValue(body));
writeResponse(ctx, resp);
}

private void badRequest(RoutingContext ctx, String verb, List<OAIPMHerrorType> errors) {
OAIPMH resp = buildBaseResponse(ctx, verb).withErrors(errors);
writeResponse(ctx, resp);
}

@Override
protected void accessDenied(RoutingContext ctx, String msg) {
log.error("accessDenied: " + msg);
Expand All @@ -203,31 +125,6 @@ protected void requestTimeout(RoutingContext ctx, String msg) {
super.requestTimeout(ctx, "The repository cannot process request. Please try later or contact administrator(s).");
}

private void getOkapiClient(RoutingContext ctx, Handler.TwoParamVoidFunction<OkapiClient, Map<String, String>> action) {
String key = keyHelper.getApiKey(ctx);
ClientInfo clientInfo;
try {
clientInfo = ApiKeyUtils.parseApiKey(key);
} catch (ApiKeyUtils.MalformedApiKeyException e) {
invalidApiKey(ctx, key);
return;
}
final OkapiClient client = ocf.getOkapiClient(clientInfo.tenantId);
iuHelper.getToken(client, clientInfo.salt, clientInfo.tenantId, clientInfo.username)
.thenAcceptAsync(token -> {
client.setToken(token);
action.apply(client, null);
})
.exceptionally(t -> {
if (t instanceof TimeoutException) {
requestTimeout(ctx, t.getMessage());
} else {
accessDenied(ctx, t.getMessage());
}
return null;
});
}

/**
* This handler-method for processing exceptional flow
*
Expand All @@ -243,29 +140,6 @@ public void oaiPmhFailureHandler(RoutingContext ctx, Throwable t) {
}
}

private void writeResponse(RoutingContext ctx, OAIPMH respBody) {
String xml = null;
try {
xml = ResponseHelper.getInstance()
.writeToString(respBody);
} catch (Exception e) {
log.error("Exception marshalling XML", e);
}
final int responseStatusCode = getErrorsProcessingConfigSetting(ctx) ? SC_OK : SC_BAD_REQUEST;
ctx.response()
.setStatusCode(responseStatusCode);

if (xml != null) {
log.warn("The request was invalid. The response returned with errors: " + xml);
ctx.response()
.putHeader(HttpHeaders.CONTENT_TYPE, TEXT_XML)
.end(xml);
} else {
ctx.response()
.end();
}
}

private void notAcceptableResponse(RoutingContext ctx, HttpServerRequest request) {
String unsupportedType = request.headers()
.getAll(ACCEPT)
Expand All @@ -274,34 +148,7 @@ private void notAcceptableResponse(RoutingContext ctx, HttpServerRequest request
.findFirst()
.orElse("");
notAcceptable(ctx,
"Accept header must be \"text/xml\" for this request, but it is " + "\"" + unsupportedType + "\"" + ", can not send */*");
}

private void serviceUnavailableResponse(RoutingContext ctx) {
ctx.response()
.setStatusCode(SC_SERVICE_UNAVAILABLE)
.putHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
.end("OAI-PMH service is disabled");
"Accept header must be \"text/xml\" for this request, but it is " + "\"" + unsupportedType + "\"" + ", can not send */*");
}

public Boolean getErrorsProcessingConfigSetting(RoutingContext ctx) {
return ctx.get(ERRORS_PROCESSING_KEY);
}

private OAIPMH buildBaseResponse(RoutingContext ctx, String verb) {
String baseUrl = StringUtils.substringBefore(ctx.request()
.absoluteURI(), "?");
MultiMap params = ctx.request()
.params();
return new OAIPMH().withResponseDate(Instant.now()
.truncatedTo(ChronoUnit.SECONDS))
.withRequest(new RequestType().withVerb(isEmpty(verb) ? null : VerbType.fromValue(verb))
.withSet(params.get(SET))
.withResumptionToken(params.get(RESUMPTION_TOKEN))
.withIdentifier(params.get(IDENTIFIER))
.withMetadataPrefix(params.get(METADATA_PREFIX))
.withFrom(params.get(FROM))
.withUntil(params.get(UNTIL))
.withValue(baseUrl));
}
}
Loading

0 comments on commit d6c84cd

Please sign in to comment.