diff options
author | Prudence Au <prudence.au@amdocs.com> | 2018-08-13 17:06:59 -0400 |
---|---|---|
committer | Pierre Rioux <pierre.rioux@amdocs.com> | 2018-08-21 11:21:26 -0400 |
commit | c604f64b971491f8c9b953adce54b847d7946e26 (patch) | |
tree | 134f7fc91b4da9e04c564c92337d44df420c8df0 /src/main/java | |
parent | 3baa3ebd0000b15f1c54c736f4a307731b16b923 (diff) |
Initial submission for validation service
Change-Id: I9372430f1ae347373d5a9a0c7a427d7bd393d61e
Issue-ID: LOG-427
Signed-off-by: Prudence Au (prudence.au@amdocs.com)
Signed-off-by: Geora Barsky <georab@amdocs.com>
Signed-off-by: Pierre Rioux <pierre.rioux@amdocs.com>
Diffstat (limited to 'src/main/java')
72 files changed, 9741 insertions, 0 deletions
diff --git a/src/main/java/org/onap/aai/auth/AAIAuthException.java b/src/main/java/org/onap/aai/auth/AAIAuthException.java new file mode 100644 index 0000000..5bbfc11 --- /dev/null +++ b/src/main/java/org/onap/aai/auth/AAIAuthException.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.auth; + +public class AAIAuthException extends Exception { + /** + * + */ + private static final long serialVersionUID = 1L; + + public AAIAuthException(String string) { + super(string); + } + + public AAIAuthException(String string, Exception e) { + super(string, e); + } + +} diff --git a/src/main/java/org/onap/aai/auth/AAIMicroServiceAuth.java b/src/main/java/org/onap/aai/auth/AAIMicroServiceAuth.java new file mode 100644 index 0000000..fc40e0b --- /dev/null +++ b/src/main/java/org/onap/aai/auth/AAIMicroServiceAuth.java @@ -0,0 +1,103 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.auth; + +import java.security.cert.X509Certificate; +import javax.inject.Inject; +import javax.security.auth.x500.X500Principal; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Cookie; +import org.onap.aai.validation.config.ValidationServiceAuthConfig; +import org.onap.aai.validation.logging.LogHelper; + +public class AAIMicroServiceAuth { + + private static LogHelper applicationLogger = LogHelper.INSTANCE; + + private ValidationServiceAuthConfig validationServiceAuthConfig; + + @Inject + public AAIMicroServiceAuth(final ValidationServiceAuthConfig validationServiceAuthConfig) throws AAIAuthException { + this.validationServiceAuthConfig = validationServiceAuthConfig; + if (!validationServiceAuthConfig.isAuthenticationDisable()) { + AAIMicroServiceAuthCore.init(validationServiceAuthConfig.getAuthPolicyFile()); + } + } + + public boolean authBasic(String username, String authFunction) throws AAIAuthException { + return AAIMicroServiceAuthCore.authorize(username, authFunction); + } + + public String authUser(String authUser, String authFunction) throws AAIAuthException { + StringBuilder username = new StringBuilder(); + + username.append(authUser); + if (!authBasic(username.toString(), authFunction)) { + return "AAI_9101"; + + } + return "OK"; + } + + public boolean authCookie(Cookie cookie, String authFunction, StringBuilder username) throws AAIAuthException { + if (cookie == null) { + return false; + } + applicationLogger.debug("Got one:" + cookie); + + return AAIMicroServiceAuthCore.authorize(username.toString(), authFunction); + } + + public boolean validateRequest(HttpServletRequest req, String action, String apiPath) throws AAIAuthException { + + applicationLogger.debug("validateRequest: " + apiPath); + applicationLogger.debug("validationServiceConfig.isAuthenticationDisable(): " + + validationServiceAuthConfig.isAuthenticationDisable()); + + if (validationServiceAuthConfig.isAuthenticationDisable()) { + return true; + } + String[] ps = apiPath.split("/"); + String authPolicyFunctionName = ps[0]; + if (ps.length > 1) { + if (ps[0].matches("v\\d+")) { + authPolicyFunctionName = ps[1]; + } else { + authPolicyFunctionName = ps[0]; + } + } + + String cipherSuite = (String) req.getAttribute("javax.servlet.request.cipher_suite"); + String authUser = null; + if (cipherSuite != null) { + X509Certificate[] certChain = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate"); + if (certChain != null) { + X509Certificate clientCert = certChain[0]; + X500Principal subjectDN = clientCert.getSubjectX500Principal(); + authUser = subjectDN.toString(); + } + } + + if (authUser == null) { + return false; + } + + String status = authUser(authUser.toLowerCase(), action + ":" + authPolicyFunctionName); + return "OK".equals(status); + } +} diff --git a/src/main/java/org/onap/aai/auth/AAIMicroServiceAuthCore.java b/src/main/java/org/onap/aai/auth/AAIMicroServiceAuthCore.java new file mode 100644 index 0000000..25273fc --- /dev/null +++ b/src/main/java/org/onap/aai/auth/AAIMicroServiceAuthCore.java @@ -0,0 +1,269 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; + +/** + * Authentication and authorization by user and role. + */ +public class AAIMicroServiceAuthCore { + + private static LogHelper applicationLogger = LogHelper.INSTANCE; + + public static final String APPCONFIG_DIR = (System.getProperty("CONFIG_HOME") == null) + ? Paths.get(System.getProperty("APP_HOME"), "appconfig").toString() : System.getProperty("CONFIG_HOME"); + + private static Path appConfigAuthDir = Paths.get(APPCONFIG_DIR, "auth"); + private static Path defaultAuthFileName = appConfigAuthDir.resolve("auth_policy.json"); + + private static boolean usersInitialized = false; + private static HashMap<String, AAIAuthUser> users; + private static boolean timerSet = false; + private static String policyAuthFileName; + + public enum HTTP_METHODS { + GET, + PUT, + DELETE, + HEAD, + POST + } + + // Don't instantiate + private AAIMicroServiceAuthCore() {} + + public static String getDefaultAuthFileName() { + return defaultAuthFileName.toString(); + } + + public static void setDefaultAuthFileName(String defaultAuthFileName) { + AAIMicroServiceAuthCore.defaultAuthFileName = Paths.get(defaultAuthFileName); + } + + public static synchronized void init(String authPolicyFile) throws AAIAuthException { + + try { + policyAuthFileName = AAIMicroServiceAuthCore.getConfigFile(authPolicyFile); + } catch (IOException e) { + applicationLogger.debug("Exception while retrieving policy file."); + applicationLogger.error(ApplicationMsgs.PROCESS_REQUEST_ERROR, e); + throw new AAIAuthException(e.getMessage()); + } + if (policyAuthFileName == null) { + throw new AAIAuthException("Auth policy file could not be found"); + } + AAIMicroServiceAuthCore.reloadUsers(); + + TimerTask task = new FileWatcher(new File(policyAuthFileName)) { + @Override + protected void onChange(File file) { + // here we implement the onChange + applicationLogger.debug("File " + file.getName() + " has been changed!"); + try { + AAIMicroServiceAuthCore.reloadUsers(); + } catch (AAIAuthException e) { + applicationLogger.error(ApplicationMsgs.PROCESS_REQUEST_ERROR, e); + } + applicationLogger.debug("File " + file.getName() + " has been reloaded!"); + } + }; + + if (!timerSet) { + timerSet = true; + Timer timer = new Timer(); + long period = TimeUnit.SECONDS.toMillis(1); + timer.schedule(task, new Date(), period); + applicationLogger.debug("Config Watcher Interval = " + period); + } + } + + public static String getConfigFile(String authPolicyFile) throws IOException { + File authFile = new File(authPolicyFile); + if (authFile.exists()) { + return authFile.getCanonicalPath(); + } + authFile = appConfigAuthDir.resolve(authPolicyFile).toFile(); + if (authFile.exists()) { + return authFile.getCanonicalPath(); + } + if (getDefaultAuthFileName() != null) { + authFile = new File(getDefaultAuthFileName()); + if (authFile.exists()) { + return getDefaultAuthFileName(); + } + } + return null; + } + + /** + * @throws AAIAuthException + */ + public static synchronized void reloadUsers() throws AAIAuthException { + users = new HashMap<>(); + ObjectMapper mapper = new ObjectMapper(); + + try { + applicationLogger.debug("Reading from " + policyAuthFileName); + JsonNode rootNode = mapper.readTree(new File(policyAuthFileName)); + JsonNode rolesNode = rootNode.path("roles"); + + for (JsonNode roleNode : rolesNode) { + String roleName = roleNode.path("name").asText(); + AAIAuthRole r = new AAIAuthRole(); + JsonNode usersNode = roleNode.path("users"); + JsonNode functionsNode = roleNode.path("functions"); + for (JsonNode functionNode : functionsNode) { + addFunctionToRole(r, roleName, functionNode); + } + for (JsonNode userNode : usersNode) { + String name = userNode.path("username").asText().toLowerCase(); + AAIAuthUser user; + if (users.containsKey(name)) { + user = users.get(name); + } else { + user = new AAIAuthUser(); + } + + applicationLogger.debug("Assigning " + roleName + " to user " + name); + user.addRole(roleName, r); + users.put(name, user); + } + } + } catch (FileNotFoundException e) { + throw new AAIAuthException("Auth policy file could not be found", e); + } catch (JsonProcessingException e) { + throw new AAIAuthException("Error processing Auth policy file ", e); + } catch (IOException e) { + throw new AAIAuthException("Error reading Auth policy file", e); + } + + usersInitialized = true; + } + + /** + * @param role + * @param roleName + * @param functionNode + */ + private static void addFunctionToRole(AAIAuthRole role, String roleName, JsonNode functionNode) { + String functionName = functionNode.path("name").asText(); + JsonNode methodsNode = functionNode.path("methods"); + + if (methodsNode.size() == 0) { + for (HTTP_METHODS method : HTTP_METHODS.values()) { + String fullFunctionName = method.toString() + ":" + functionName; + applicationLogger.debug("Installing (all methods) " + fullFunctionName + " on role " + roleName); + role.addAllowedFunction(fullFunctionName); + } + } else { + for (JsonNode methodNode : methodsNode) { + String methodName = methodNode.path("name").asText(); + String fullFunctionName = methodName + ":" + functionName; + applicationLogger.debug("Installing function " + fullFunctionName + " on role " + roleName); + role.addAllowedFunction(fullFunctionName); + } + } + } + + public static class AAIAuthUser { + private HashMap<String, AAIAuthRole> roles; + + public AAIAuthUser() { + this.roles = new HashMap<>(); + } + + public void addRole(String roleName, AAIAuthRole r) { + this.roles.put(roleName, r); + } + + public boolean checkAllowed(String checkFunc) { + for (Entry<String, AAIAuthRole> role_entry : roles.entrySet()) { + AAIAuthRole role = role_entry.getValue(); + if (role.hasAllowedFunction(checkFunc)) { + return true; + } + } + return false; + } + + } + + public static class AAIAuthRole { + private List<String> allowedFunctions; + + public AAIAuthRole() { + this.allowedFunctions = new ArrayList<>(); + } + + public void addAllowedFunction(String func) { + this.allowedFunctions.add(func); + } + + public void delAllowedFunction(String delFunc) { + if (this.allowedFunctions.contains(delFunc)) { + this.allowedFunctions.remove(delFunc); + } + } + + public boolean hasAllowedFunction(String functionName) { + return allowedFunctions.contains(functionName) ? true : false; + } + } + + public static boolean authorize(String username, String authFunction) throws AAIAuthException { + if (!usersInitialized || users == null) { + throw new AAIAuthException("Auth module not initialized"); + } + + if (users.containsKey(username)) { + if (users.get(username).checkAllowed(authFunction)) { + logAuthenticationResult(username, authFunction, "AUTH ACCEPTED"); + return true; + } else { + logAuthenticationResult(username, authFunction, "AUTH FAILED"); + return false; + } + } else { + logAuthenticationResult(username, authFunction, "User not found"); + return false; + } + } + + private static void logAuthenticationResult(String username, String authFunction, String result) { + applicationLogger.debug(result + ": " + username + " on function " + authFunction); + } + +} diff --git a/src/main/java/org/onap/aai/auth/FileWatcher.java b/src/main/java/org/onap/aai/auth/FileWatcher.java new file mode 100644 index 0000000..edc8bfe --- /dev/null +++ b/src/main/java/org/onap/aai/auth/FileWatcher.java @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.auth; + +import java.io.File; +import java.util.TimerTask; + +public abstract class FileWatcher extends TimerTask { + private long timeStamp; + private File file; + + /** + * Instantiates a new file watcher. + * + * @param file the file + */ + public FileWatcher(File file) { + this.file = file; + this.timeStamp = file.lastModified(); + } + + /** + * runs a timer task + * + * @see java.util.TimerTask.run + */ + @Override + public final void run() { + long newTimeStamp = file.lastModified(); + + if ((newTimeStamp - this.timeStamp) > 500) { + this.timeStamp = newTimeStamp; + onChange(file); + } + } + + /** + * On change. + * + * @param file the file + */ + protected abstract void onChange(File file); +} diff --git a/src/main/java/org/onap/aai/validation/ValidationServiceApplication.java b/src/main/java/org/onap/aai/validation/ValidationServiceApplication.java new file mode 100644 index 0000000..8bf7a44 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ValidationServiceApplication.java @@ -0,0 +1,57 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation; + +import org.eclipse.jetty.util.security.Password; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ImportResource; + +import java.util.HashMap; + + +/** + * Validation Service Spring Boot Application. + */ + +@SpringBootApplication +@ComponentScan(basePackages = "org.onap.aai.validation") +@ImportResource("classpath:validation-service-beans.xml") +public class ValidationServiceApplication extends SpringBootServletInitializer { + + // public static void main(String[] args) { + // SpringApplication.run(ValidationServiceApplication.class, args); + // } + + + public static void main(String[] args) { + HashMap<String, Object> props = new HashMap<>(); + String keyStorePassword = System.getProperty("KEY_STORE_PASSWORD"); + if (keyStorePassword != null && !keyStorePassword.isEmpty()) { + props.put("server.ssl.key-store-password", Password.deobfuscate(keyStorePassword)); + } + new ValidationServiceApplication() + .configure(new SpringApplicationBuilder(ValidationServiceApplication.class).properties(props)) + .run(args); + } + + +} diff --git a/src/main/java/org/onap/aai/validation/Validator.java b/src/main/java/org/onap/aai/validation/Validator.java new file mode 100644 index 0000000..c3ecc2a --- /dev/null +++ b/src/main/java/org/onap/aai/validation/Validator.java @@ -0,0 +1,45 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation; + +import java.util.List; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.result.ValidationResult; + +/** + * Validator (e.g. model-driven or rule-based) + * + */ +public interface Validator { + + /** + * This method should be called (once) before validate() to ensure that all configuration is correctly loaded. + * + * @throws ValidationServiceException + */ + public void initialise() throws ValidationServiceException; + + /** + * Validate the entity or entities found in the event. + * + * @param event JSON containing the entity or entities to validate + * @return a list of validation results + * @throws ValidationServiceException + */ + public List<ValidationResult> validate(String event) throws ValidationServiceException; +} diff --git a/src/main/java/org/onap/aai/validation/config/EventReaderConfig.java b/src/main/java/org/onap/aai/validation/config/EventReaderConfig.java new file mode 100644 index 0000000..c819351 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/EventReaderConfig.java @@ -0,0 +1,163 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import org.springframework.beans.factory.annotation.Value; + +/** + * Loaded via Spring from src/main/resources/event-reader.properties. + */ +public class EventReaderConfig extends PropertiesConfig { + + @Value("${event.domain.path}") + private String eventDomainPath; + + @Value("${event.action.path}") + private String eventActionPath; + + @Value("${event.type.path}") + private String eventTypePath; + + @Value("${event.entity.type.path}") + private String entityTypePath; + + @Value("${event.entity.type.top.path}") + private String topEntityTypePath; + + @Value("${event.entity.link.path}") + private String entityLinkPath; + + @Value("${event.entity.link.delimiter}") + private String entityLinkDelimiter; + + @Value("${event.entity.path}") + private String entityPath; + + @Value("${event.entity.nested.path}") + private String nestedEntityPath; + + /** Entity relative path. Use when the entity has been extracted from the event. */ + @Value("${entity.id.path}") + private String entityIdPath; + + /** Entity relative path. Use when the entity has been extracted from the event. */ + @Value("${entity.resource.version.path}") + private String entityResourceVersionPath; + + public String getEventDomainPath() { + return eventDomainPath; + } + + public void setEventDomainPath(String eventDomainPath) { + this.eventDomainPath = eventDomainPath; + } + + public String getEventActionPath() { + return eventActionPath; + } + + public void setEventActionPath(String eventActionPath) { + this.eventActionPath = eventActionPath; + } + + public String getEventTypePath() { + return eventTypePath; + } + + public void setEventTypePath(String eventTypePath) { + this.eventTypePath = eventTypePath; + } + + public String getTopEntityTypePath() { + return topEntityTypePath; + } + + public void setTopEntityTypePath(String topEntityTypePath) { + this.topEntityTypePath = topEntityTypePath; + } + + public String getEntityLinkPath() { + return entityLinkPath; + } + + public void setEntityLinkPath(String entityLinkPath) { + this.entityLinkPath = entityLinkPath; + } + + public String getEntityLinkDelimiter() { + return entityLinkDelimiter; + } + + public void setEntityLinkDelimiter(String entityLinkDelimiter) { + this.entityLinkDelimiter = entityLinkDelimiter; + } + + public String getEntityTypePath() { + return entityTypePath; + } + + public void setEntityTypePath(String entityTypePath) { + this.entityTypePath = entityTypePath; + } + + public String getEntityPath() { + return entityPath; + } + + public void setEntityPath(String entityPath) { + this.entityPath = entityPath; + } + + /** + * Formats the nested entity path using the entity type provided. + * + * @param entityType + * an entity type + * @return the formatted nested entity path + */ + public String getNestedEntityPath(String entityType) { + return formatter(nestedEntityPath, entityType); + } + + public void setNestedEntityPath(String nestedEntityPath) { + this.nestedEntityPath = nestedEntityPath; + } + + /** + * Formats the entity ID path using the entity type provided. + * + * @param entityType + * an entity type + * @return the formatted entity ID path + */ + public String getEntityIdPath(String entityType) { + return formatter(entityIdPath, entityType); + } + + public void setEntityIdPath(String entityIdPath) { + this.entityIdPath = entityIdPath; + } + + public String getEntityResourceVersionPath() { + return entityResourceVersionPath; + } + + public void setEntityResourceVersionPath(String entityResourceVersionPath) { + this.entityResourceVersionPath = entityResourceVersionPath; + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/config/ModelConfig.java b/src/main/java/org/onap/aai/validation/config/ModelConfig.java new file mode 100644 index 0000000..02e14bc --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/ModelConfig.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import org.springframework.beans.factory.annotation.Value; + +/** + * Model Configuration Bean + * + */ +public class ModelConfig { + + @Value("${model.cache.expirySeconds}") + private Long modelCacheExpirySeconds; + + public Long getModelCacheExpirySeconds() { + return modelCacheExpirySeconds; + } + + public void setModelCacheExpirySeconds(Long modelCacheExpirySeconds) { + this.modelCacheExpirySeconds = modelCacheExpirySeconds; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/config/PropertiesConfig.java b/src/main/java/org/onap/aai/validation/config/PropertiesConfig.java new file mode 100644 index 0000000..cf4bcb2 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/PropertiesConfig.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import java.text.MessageFormat; + +/** + * Base properties configuration class. + */ +public class PropertiesConfig { + + /** + * Replaces place-holders in property values. + * + * @param s + * a string with place-holders in the form {n} + * @param args + * values for place-holders + * @return a formated String with replaced place-holders. + */ + public String formatter(String s, Object... args) { + MessageFormat formatter = new MessageFormat(""); + formatter.applyPattern(s); + return formatter.format(args); + } +} diff --git a/src/main/java/org/onap/aai/validation/config/RestConfig.java b/src/main/java/org/onap/aai/validation/config/RestConfig.java new file mode 100644 index 0000000..e99d6c2 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/RestConfig.java @@ -0,0 +1,225 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import java.util.Objects; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.springframework.beans.factory.annotation.Value; + +/** + * Configuration required to establish REST client requests with an application. + */ +public class RestConfig { + + @Value("${host}") + private String host; + + @Value("${port}") + private Integer port; + + @Value("${httpProtocol}") + private String protocol; + + @Value("${baseModelURI}") + private String baseModelURI; + + @Value("${trustStorePath}") + private String trustStorePath; + + @Value("${trustStorePassword.x}") + private String trustStorePassword; + + @Value("${keyStorePath}") + private String keyStorePath; + + @Value("${keyStorePassword.x}") + private String keyStorePassword; + + @Value("${keyManagerFactoryAlgorithm}") + private String keyManagerFactoryAlgorithm; + + @Value("${keyStoreType}") + private String keyStoreType; + + @Value("${securityProtocol}") + private String securityProtocol; + + @Value("${connectionTimeout}") + private Integer connectionTimeout; + + @Value("${readTimeout}") + private Integer readTimeout; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getBaseModelURI() { + return baseModelURI; + } + + public void setBaseModelURI(String baseModelURI) { + this.baseModelURI = baseModelURI; + } + + public String getTrustStorePath() { + return trustStorePath; + } + + public void setTrustStorePath(String trustStorePath) { + this.trustStorePath = trustStorePath; + } + + /** + * Assumes the password is encrypted. + * + * @return the decrypted password + */ + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getKeyStorePath() { + return keyStorePath; + } + + public void setKeyStorePath(String keyStorePath) { + this.keyStorePath = keyStorePath; + } + + /** + * Assumes the password is encrypted. + * + * @return the decrypted password + */ + public String getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getKeyManagerFactoryAlgorithm() { + return keyManagerFactoryAlgorithm; + } + + public void setKeyManagerFactoryAlgorithm(String keyManagerFactoryAlgorithm) { + this.keyManagerFactoryAlgorithm = keyManagerFactoryAlgorithm; + } + + public String getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public String getSecurityProtocol() { + return securityProtocol; + } + + public void setSecurityProtocol(String securityProtocol) { + this.securityProtocol = securityProtocol; + } + + public Integer getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(Integer connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Integer getReadTimeout() { + return readTimeout; + } + + public void setReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + } + + @Override + public String toString() { + return "RestConfig [host=" + host + ", port=" + port + ", protocol=" + protocol + ", baseModelURI=" + + baseModelURI + ", trustStorePath=" + trustStorePath + ", trustStorePassword=" + trustStorePassword + + ", keyStorePath=" + keyStorePath + ", keyStorePassword=" + keyStorePassword + + ", keyManagerFactoryAlgorithm=" + keyManagerFactoryAlgorithm + ", keyStoreType=" + keyStoreType + + ", securityProtocol=" + securityProtocol + ", connectionTimeout=" + connectionTimeout + + ", readTimeout=" + readTimeout + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(this.baseModelURI, this.connectionTimeout, this.host, this.keyManagerFactoryAlgorithm, + this.keyStorePassword, this.keyStorePath, this.keyStoreType, this.port, this.protocol, this.readTimeout, + this.securityProtocol, this.trustStorePassword, this.trustStorePath); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RestConfig)) { + return false; + } else if (obj == this) { + return true; + } + RestConfig rhs = (RestConfig) obj; + // @formatter:off + return new EqualsBuilder() + .append(baseModelURI, rhs.baseModelURI) + .append(connectionTimeout, rhs.connectionTimeout) + .append(host, rhs.host) + .append(keyManagerFactoryAlgorithm, rhs.keyManagerFactoryAlgorithm) + .append(keyStorePassword, rhs.keyStorePassword) + .append(keyStorePath, rhs.keyStorePath) + .append(keyStoreType, rhs.keyStoreType) + .append(port, rhs.port) + .append(protocol, rhs.protocol) + .append(readTimeout, rhs.readTimeout) + .append(securityProtocol, rhs.securityProtocol) + .append(trustStorePassword, rhs.trustStorePassword) + .append(trustStorePath, rhs.trustStorePath) + .isEquals(); + // @formatter:on + } +} diff --git a/src/main/java/org/onap/aai/validation/config/RuleIndexingConfig.java b/src/main/java/org/onap/aai/validation/config/RuleIndexingConfig.java new file mode 100644 index 0000000..1d27705 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/RuleIndexingConfig.java @@ -0,0 +1,73 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import java.util.List; + +/** + * Loads the properties needed by the controller using spring. + */ +public class RuleIndexingConfig extends PropertiesConfig { + + private List<String> indexedEvents; + + private List<String> excludedOxmValidationEvents; + + private List<String> indexAttributes; + + private String defaultIndexKey; + + public List<String> getIndexedEvents() { + return indexedEvents; + } + + public void setIndexedEvents(List<String> indexedEvents) { + this.indexedEvents = indexedEvents; + } + + public List<String> getExcludedOxmValidationEvents() { + return excludedOxmValidationEvents; + } + + public void setExcludedOxmValidationEvents(List<String> excludedOxmValidationEvents) { + this.excludedOxmValidationEvents = excludedOxmValidationEvents; + } + + public List<String> getIndexAttributes() { + return indexAttributes; + } + + public void setIndexAttributes(List<String> indexAttributes) { + this.indexAttributes = indexAttributes; + } + + public String getDefaultIndexKey() { + return defaultIndexKey; + } + + public void setDefaultIndexKey(String defaultIndexKey) { + this.defaultIndexKey = defaultIndexKey; + } + + public boolean skipOxmValidation(String event) { + if(excludedOxmValidationEvents == null) { + return false; + } + return excludedOxmValidationEvents.contains(event); + } +} diff --git a/src/main/java/org/onap/aai/validation/config/TopicAdminConfig.java b/src/main/java/org/onap/aai/validation/config/TopicAdminConfig.java new file mode 100644 index 0000000..22fbe97 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/TopicAdminConfig.java @@ -0,0 +1,103 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import java.util.Objects; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.springframework.beans.factory.annotation.Value; + +/** + * Configuration bean with topic administration properties that are loaded via Spring configuration. + */ +public class TopicAdminConfig { + + @Value("${topic.publish.enable}") + private boolean publishEnable; + + @Value("${topic.publish.retries}") + private Long publishRetries; + + @Value("${topic.consume.enable}") + private boolean consumeEnable; + + @Value("${topic.consume.polling.interval.seconds}") + private Long consumePollingIntervalSeconds; + + public boolean isPublishEnable() { + return publishEnable; + } + + public void setPublishEnable(boolean publishEnable) { + this.publishEnable = publishEnable; + } + + public Long getPublishRetries() { + return publishRetries; + } + + public void setPublishRetries(Long publishRetries) { + this.publishRetries = publishRetries; + } + + public boolean isConsumeEnable() { + return consumeEnable; + } + + public void setConsumeEnable(boolean consumeEnable) { + this.consumeEnable = consumeEnable; + } + + public Long getConsumePollingIntervalSeconds() { + return consumePollingIntervalSeconds; + } + + public void setConsumePollingIntervalSeconds(Long consumePollingIntervalSeconds) { + this.consumePollingIntervalSeconds = consumePollingIntervalSeconds; + } + + @Override + public int hashCode() { + return Objects.hash(this.consumeEnable, this.consumePollingIntervalSeconds, this.publishEnable, + this.publishRetries); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TopicAdminConfig)) { + return false; + } else if (obj == this) { + return true; + } + TopicAdminConfig rhs = (TopicAdminConfig) obj; + // @formatter:off + return new EqualsBuilder() + .append(consumeEnable, rhs.consumeEnable) + .append(consumePollingIntervalSeconds, rhs.consumePollingIntervalSeconds) + .append(publishEnable, rhs.publishEnable) + .append(publishRetries, rhs.publishRetries) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return "TopicAdminConfig [publishEnable=" + publishEnable + ", publishRetries=" + publishRetries + + ", consumeEnable=" + consumeEnable + ", consumePollingIntervalSeconds=" + + consumePollingIntervalSeconds + "]"; + } +} diff --git a/src/main/java/org/onap/aai/validation/config/TopicConfig.java b/src/main/java/org/onap/aai/validation/config/TopicConfig.java new file mode 100644 index 0000000..99742ac --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/TopicConfig.java @@ -0,0 +1,243 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import javax.annotation.Resource; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Gets the configuration of the topics. The topics are configured using Spring in topic-config-beans.xml. + */ + +@Component("TopicConfig") +public class TopicConfig { + + private List<String> consumerTopicNames; + + private List<String> publisherTopicNames; + + @Resource(name = "topicProperties") + private Properties topicProperties; + + List<Topic> consumerTopics = new ArrayList<>(); + List<Topic> publisherTopics = new ArrayList<>(); + + @Autowired + public TopicConfig (@Value("${consumer.topic.names}") final String consumerNames, @Value("${publisher.topic.names}") final String publisherNames){ + + consumerTopicNames = Arrays.asList(consumerNames.split(","));; + publisherTopicNames = Arrays.asList(publisherNames.split(","));; + + + } + /** + * Gets the configuration of topics for consumption. + * + * @return a list of topic configurations. + */ + public List<Topic> getConsumerTopics() + { + return populateTopics(consumerTopics, consumerTopicNames); + } + + + /** + * Gets the configuration of topics for publishing. + * + * @return a list of topic configurations. + */ + public List<Topic> getPublisherTopics() { + return populateTopics(publisherTopics, publisherTopicNames); + } + + /** + * Populates the topics list with topic objects created from each item in the topicNames list. + * + * @param topics + * The topic list to populate. + * @param topicNames + * The list of topic names to populate the topic list with. + * @return The populated topic list. + */ + private List<Topic> populateTopics(List<Topic> topics, List<String> topicNames) { + if (topics.isEmpty()) { + for (String topicName : topicNames) { + Topic topicConfig = new Topic(); + topicConfig.setName(getTopicProperties().getProperty(topicName + ".name")); + topicConfig.setHost(getTopicProperties().getProperty(topicName + ".host")); + topicConfig.setUsername(getTopicProperties().getProperty(topicName + ".username")); + topicConfig.setPassword(getTopicProperties().getProperty(topicName + ".password")); + topicConfig.setPartition(getTopicProperties().getProperty(topicName + ".publisher.partition")); + topicConfig.setConsumerGroup(getTopicProperties().getProperty(topicName + ".consumer.group")); + topicConfig.setConsumerId(getTopicProperties().getProperty(topicName + ".consumer.id")); + topicConfig.setTransportType(getTopicProperties().getProperty(topicName + ".transport.type")); + topics.add(topicConfig); + } + } + return topics; + } + + public List<String> getConsumerTopicNames() { + return consumerTopicNames; + } + + public void setConsumerTopicNames(List<String> consumerTopicNames) { + this.consumerTopicNames = consumerTopicNames; + } + + public List<String> getPublisherTopicNames() { + return publisherTopicNames; + } + + public void setPublisherTopicNames(List<String> publisherTopicNames) { + this.publisherTopicNames = publisherTopicNames; + } + + public Properties getTopicProperties() { + return topicProperties; + } + + public void setTopicProperties(Properties topicProperties) { + this.topicProperties = topicProperties; + } + + /** + * Defines the properties of a single topic for consumption. + */ + public class Topic { + private String name; + private String host; + private String username; + private String password; + private String partition; + private String consumerGroup; + private String consumerId; + private String transportType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPartition() { + return partition; + } + + public void setPartition(String partition) { + this.partition = partition; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getConsumerId() { + return consumerId; + } + + public void setConsumerId(String consumerId) { + this.consumerId = consumerId; + } + + public List<String> getHosts() { + return Arrays.asList(host.split(",")); + } + + public String getTransportType() { + return transportType; + } + + public void setTransportType(String transportType) { + this.transportType = transportType; + } + + @Override + public int hashCode() { + return Objects.hash(this.consumerGroup, this.consumerId, this.host, this.username, this.name, this.partition, + this.password, this.transportType); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Topic)) { + return false; + } else if (obj == this) { + return true; + } + Topic rhs = (Topic) obj; + // @formatter:off + return new EqualsBuilder() + .append(consumerGroup, rhs.consumerGroup) + .append(consumerId, rhs.consumerId) + .append(host, rhs.host) + .append(username, rhs.username) + .append(name, rhs.name) + .append(partition, rhs.partition) + .append(password, rhs.password) + .append(transportType, rhs.transportType) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return "Topic [name=" + name + ", host=" + host + ", username=" + username + ", password=" + password + ", partition=" + + partition + ", consumerGroup=" + consumerGroup + ", consumerId=" + consumerId + + ", transportType =" + transportType + "]"; + } + } +} diff --git a/src/main/java/org/onap/aai/validation/config/TopicPropertiesConfig.java b/src/main/java/org/onap/aai/validation/config/TopicPropertiesConfig.java new file mode 100644 index 0000000..4742cef --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/TopicPropertiesConfig.java @@ -0,0 +1,75 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import org.onap.aai.validation.logging.LogHelper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +@Configuration +public class TopicPropertiesConfig { + + private static LogHelper applicationLogger = LogHelper.INSTANCE; + + @Value("${topics.properties.location}") + private String topicsPropertiesLocation; + + private static final String[] TOPICS_PROPERTIES_LOCATION_TPL = { "file:./%s/*.properties", "classpath:/%s/*.properties" }; + + @Bean(name="topicProperties") + public Properties topicProperties() throws IOException { + PropertiesFactoryBean config = new PropertiesFactoryBean(); + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + List<Resource> resouceList = new ArrayList<Resource>(); + try { + for (String p : bldrsPropLoc2Path(topicsPropertiesLocation)) { + Resource[] resources = resolver.getResources(p); + if (resources != null && resources.length > 0) { + for (Resource resource : resources) { + resouceList.add(resource); + } + break; + } + } + } catch (Exception e) { + applicationLogger.logAuditError(e); + } + config.setLocations(resouceList.toArray(new Resource[]{})); + config.afterPropertiesSet(); + return config.getObject(); + } + + private static String[] bldrsPropLoc2Path(String topicsPropertiesLocation) { + String[] res = new String[TOPICS_PROPERTIES_LOCATION_TPL.length]; + int indx = 0; + for (String tmpl : TOPICS_PROPERTIES_LOCATION_TPL) { + res[indx++] = String.format(tmpl, topicsPropertiesLocation).replace("//", "/"); + } + return res; + } + +} diff --git a/src/main/java/org/onap/aai/validation/config/ValidationControllerConfig.java b/src/main/java/org/onap/aai/validation/config/ValidationControllerConfig.java new file mode 100644 index 0000000..4110175 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/ValidationControllerConfig.java @@ -0,0 +1,82 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import java.util.List; +import org.springframework.beans.factory.annotation.Value; + +/** + * Loads the properties needed by the controller using spring. + */ +public class ValidationControllerConfig extends PropertiesConfig { + + @Value("${event.domain}") + private String eventDomain; + + @Value("#{'${event.action.exclude}'.split(',')}") + private List<String> excludedEventActions; + + @Value("#{'${event.type.rule}'.split(',')}") + private List<String> eventTypeRule; + + @Value("#{'${event.type.model}'.split(',')}") + private List<String> eventTypeModel; + + @Value("${event.type.end:END-EVENT}") + private String eventTypeEnd; + + public String getEventDomain() { + return eventDomain; + } + + public void setEventDomain(String eventDomain) { + this.eventDomain = eventDomain; + } + + public List<String> getExcludedEventActions() { + return excludedEventActions; + } + + public void setExcludedEventActions(List<String> excludedEventActions) { + this.excludedEventActions = excludedEventActions; + } + + public List<String> getEventTypeRule() { + return eventTypeRule; + } + + public void setEventTypeRule(List<String> eventTypeRule) { + this.eventTypeRule = eventTypeRule; + } + + public List<String> getEventTypeModel() { + return eventTypeModel; + } + + public void setEventTypeModel(List<String> eventTypeModel) { + this.eventTypeModel = eventTypeModel; + } + + public String getEventTypeEnd() { + return eventTypeEnd; + } + + public void setEventTypeEnd(String eventTypeEnd) { + this.eventTypeEnd = eventTypeEnd; + } +} diff --git a/src/main/java/org/onap/aai/validation/config/ValidationServiceAuthConfig.java b/src/main/java/org/onap/aai/validation/config/ValidationServiceAuthConfig.java new file mode 100644 index 0000000..62280fa --- /dev/null +++ b/src/main/java/org/onap/aai/validation/config/ValidationServiceAuthConfig.java @@ -0,0 +1,46 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.config; + +import org.springframework.beans.factory.annotation.Value; + +public class ValidationServiceAuthConfig { + + @Value("${auth.authentication.disable}") + private boolean authenticationDisable; + + @Value("${auth.policy.file}") + private String authPolicyFile; + + public boolean isAuthenticationDisable() { + return authenticationDisable; + } + + public void setAuthenticationDisable(boolean authenticationDisable) { + this.authenticationDisable = authenticationDisable; + } + + public String getAuthPolicyFile() { + return authPolicyFile; + } + + public void setAuthPolicyFile(String authPolicyFile) { + this.authPolicyFile = authPolicyFile; + } + +} diff --git a/src/main/java/org/onap/aai/validation/controller/ValidationController.java b/src/main/java/org/onap/aai/validation/controller/ValidationController.java new file mode 100644 index 0000000..60d1c1c --- /dev/null +++ b/src/main/java/org/onap/aai/validation/controller/ValidationController.java @@ -0,0 +1,380 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.controller; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import javax.inject.Inject; +import org.onap.aai.validation.Validator; +import org.onap.aai.validation.config.ValidationControllerConfig; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.publisher.MessagePublisher; +import org.onap.aai.validation.reader.EventReader; +import org.onap.aai.validation.reader.data.Entity; +import org.onap.aai.validation.result.ValidationResult; +import org.onap.aai.validation.result.Violation; +import org.onap.aai.validation.util.JsonUtil; + +/** + * Controls the execution (of validation of an event) for the various validation service components. + */ +public class ValidationController { + + private static LogHelper applicationLogger = LogHelper.INSTANCE; + + public static final String VALIDATION_ERROR_SEVERITY = "CRITICAL"; + + private static final String VALIDATION_ERROR_CATEGORY = "CANNOT_VALIDATE"; + private static final String VALIDATION_ERROR_VIOLATIONTYPE = "NONE"; + + /** + * Result of the Controller executing validation of a single event. Either there is a set of ValidationResults or + * instead an exception was handled. + */ + public class Result { + /** + * A successful validation will produce a set of results. + */ + Optional<List<ValidationResult>> validationResults = Optional.empty(); + + /** + * For an unsuccessful validation, we will record the error details. + */ + private String errorText; + + /** + * @return whether or not we have a set of validation results + */ + public boolean validationSuccessful() { + return validationResults.isPresent(); + } + + public List<ValidationResult> getValidationResults() { + return validationResults.orElse(Collections.emptyList()); + } + + /** + * @return a JSON string representing the first ValidationResult, or an empty string when there are no results + */ + public String getValidationResultAsJson() { + List<ValidationResult> resultsList = getValidationResults(); + if (resultsList.isEmpty()) { + return ""; + } else { + // Only one Validation Result is returned (as only one is expected) + return JsonUtil.toJson(resultsList.get(0)); + } + } + + public Optional<String> getErrorText() { + return Optional.ofNullable(errorText); + } + + private void handleException(String event, Exception rootException) { + try { + Entity entity = eventReader.getEntity(event); + if (!entity.getIds().isEmpty() && eventReader.getEntityType(event).isPresent() + && entity.getResourceVersion().isPresent()) { + ValidationResult validationResult = new ValidationResult(entity); + // @formatter:off + validationResult.addViolation(new Violation.Builder(entity) + .category(VALIDATION_ERROR_CATEGORY) + .severity(VALIDATION_ERROR_SEVERITY) + .violationType(VALIDATION_ERROR_VIOLATIONTYPE) + .errorMessage(rootException.getMessage()) + .build()); + // @formatter:on + + validationResults = Optional.of(Collections.singletonList(validationResult)); + publishValidationResults(validationResults); + } + } catch (Exception e) { + errorText = e.getMessage(); + applicationLogger.error(ApplicationMsgs.CANNOT_VALIDATE_HANDLE_EXCEPTION_ERROR, e, event); + } + } + } + + /** + * Status Report for the Controller + * + */ + public class StatusReport { + + private Temporal reportTime = LocalDateTime.now(); + private long upTime = ChronoUnit.SECONDS.between(startTime, reportTime); + private long upTimeDays = ChronoUnit.DAYS.between(startTime, reportTime); + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Started at "); + sb.append(startTime).append('\n').append("Up time "); + if (upTimeDays > 0) { + sb.append(upTimeDays).append(" days "); + } + sb.append(LocalTime.MIDNIGHT.plusSeconds(upTime).format(DateTimeFormatter.ofPattern("HH:mm:ss"))); + sb.append('\n').append(formatStats(stats)); + return sb.toString(); + } + + /** + * @return formatted statistics + */ + private String formatStats(Statistics stats) { + StringBuilder sb = new StringBuilder(); + formatStats(stats, "info", sb, "Info Service"); + formatStats(stats, "http", sb, "Validation REST API"); + formatStats(stats, "topic", sb, "Events Consumed"); + if (stats.reportedThrowable != null) { + StringWriter sw = new StringWriter(); + stats.reportedThrowable.printStackTrace(new PrintWriter(sw)); + sb.append("Exception reported: ").append(sw.toString()); + } + return sb.toString(); + } + + private void formatStats(Statistics stats, String eventSource, StringBuilder sb, String heading) { + sb.append("\n").append(heading).append("\n"); + if (stats.keyValues(eventSource).isEmpty()) { + sb.append("total=0\n"); + } else { + for (String key : stats.keyValues(eventSource)) { + sb.append(key).append("=").append(stats.messageCount(eventSource, key)).append("\n"); + } + } + } + } + + private ValidationControllerConfig validationControllerConfig; + private EventReader eventReader; + private Validator ruleDrivenValidator; + private Validator modelDrivenValidator; + private MessagePublisher messagePublisher; + private LocalDateTime startTime; + private Statistics stats; + + /** + * Record of actions taken by the Controller + * + */ + private class Statistics { + + private Map<String, SortedMap<String, Integer>> sourceMap = new HashMap<>(); + private Throwable reportedThrowable; + + /** + * Increment the message count for the composite key <eventSource, key> + * + * @param eventSource the source of the event - used for statistics reporting purposes + * @param key the statistic to increment by one + */ + private void incrementEventCount(String eventSource, String key) { + Map<String, Integer> messagesConsumed = getMessageCountsMap(eventSource); + int count = messagesConsumed.getOrDefault(key, 0); + messagesConsumed.put(key, count + 1); + } + + private Map<String, Integer> getMessageCountsMap(String eventSource) { + return sourceMap.computeIfAbsent(eventSource, k -> new TreeMap<>()); + } + + /** + * @param eventSource the source of the event + * @return List<String> the keys for the specified eventSource + */ + private List<String> keyValues(String eventSource) { + Map<String, Integer> messagesConsumed = getMessageCountsMap(eventSource); + return new ArrayList<>(messagesConsumed.keySet()); + } + + /* + * return the count for the supplied event source and statistic key + */ + private int messageCount(String eventSource, String key) { + Map<String, Integer> messagesConsumed = getMessageCountsMap(eventSource); + return messagesConsumed.getOrDefault(key, 0); + } + + } + + /** + * Constructs a new validation controller with the injected parameters. + * + * @param validationControllerConfig the configuration parameters for validation controllers + * @param eventReader an object that can read events + * @param ruleDrivenValidator a validator for validating rules + * @param modelDrivenValidator a validator for validating model + * @param messagePublisher an instance of a publisher for messages + */ + @Inject + public ValidationController(ValidationControllerConfig validationControllerConfig, EventReader eventReader, + Validator ruleDrivenValidator, Validator modelDrivenValidator, MessagePublisher messagePublisher) { + this.startTime = LocalDateTime.now(); + this.validationControllerConfig = validationControllerConfig; + this.eventReader = eventReader; + this.ruleDrivenValidator = ruleDrivenValidator; + this.modelDrivenValidator = modelDrivenValidator; + this.messagePublisher = messagePublisher; + this.stats = new Statistics(); + } + + /** + * @throws ValidationServiceException if an error occurs initialising the controller + */ + public void initialise() throws ValidationServiceException { + ruleDrivenValidator.initialise(); + modelDrivenValidator.initialise(); + } + + /** + * Validates the event and publishes the results of the validation onto the topic configured in the message + * publisher. + * + * @param event the event to be validated + * @param eventSource the source of the event + * @return Result a result containing either the set of ValidationResults or an error message + */ + public Result execute(String event, String eventSource) { + Result result = new Result(); + try { + stats.incrementEventCount(eventSource, "total"); + if (isEndEvent(event)) { + applicationLogger.debug("Event has not been processed. End event type was detected. Event :" + event); + stats.incrementEventCount(eventSource, "end"); + } else if (isValidationCandidate(event)) { + result.validationResults = dispatchEvent(event, eventSource); + publishValidationResults(result.validationResults); + } else { + stats.incrementEventCount(eventSource, "filtered"); + } + } catch (Exception e) { + applicationLogger.error(ApplicationMsgs.CANNOT_VALIDATE_ERROR, e, event); + stats.incrementEventCount(eventSource, "errored"); + result.handleException(event, e); + } + return result; + } + + private void publishValidationResults(Optional<List<ValidationResult>> validationResults) { + if (validationResults.isPresent()) { + for (ValidationResult validationResult : validationResults.get()) { + try { + messagePublisher.publishMessage(validationResult.toJson()); + } catch (ValidationServiceException e) { + applicationLogger.error(ApplicationMsgs.MESSAGE_PUBLISH_ERROR, e, validationResult.toString()); + } + } + } + } + + private Optional<List<ValidationResult>> dispatchEvent(String event, String eventSource) + throws ValidationServiceException { + List<ValidationResult> validationResults = null; + Optional<String> eventType = eventReader.getEventType(event); + + applicationLogger.debug("Event consumed: " + event); + + if (eventType.isPresent()) { + if (isRuleDriven(eventType.get())) { + validationResults = ruleDrivenValidator.validate(event); + stats.incrementEventCount(eventSource, "rule"); + } else if (isModelDriven(eventType.get())) { + validationResults = modelDrivenValidator.validate(event); + stats.incrementEventCount(eventSource, "model"); + } else { + applicationLogger.debug("Event has not been validated. Invalid event type. Event :" + event); + stats.incrementEventCount(eventSource, "invalid"); + } + } else { + stats.incrementEventCount(eventSource, "missing event type"); + } + + return Optional.ofNullable(validationResults); + } + + private Boolean isRuleDriven(String eventType) { + return validationControllerConfig.getEventTypeRule().contains(eventType); + } + + private Boolean isModelDriven(String eventType) { + return validationControllerConfig.getEventTypeModel().contains(eventType); + } + + private boolean isEndEvent(String event) throws ValidationServiceException { + Optional<String> eventType = eventReader.getEventType(event); + + return eventType.isPresent() && "END-EVENT".equalsIgnoreCase(eventType.get()); + } + + private Boolean isDomainValid(String event) throws ValidationServiceException { + Optional<String> eventDomain = eventReader.getEventDomain(event); + + // Domain is optional in Event Header + return !eventDomain.isPresent() + || validationControllerConfig.getEventDomain().equalsIgnoreCase(eventDomain.get()); + } + + private Boolean isNotExcludedAction(String event) throws ValidationServiceException { + Optional<String> eventAction = eventReader.getEventAction(event); + + // Action is optional in Event Header + return !eventAction.isPresent() + || !validationControllerConfig.getExcludedEventActions().contains(eventAction.get()); + } + + + private Boolean isValidationCandidate(String event) throws ValidationServiceException { + return isDomainValid(event) && isNotExcludedAction(event); + } + + /** + * @return a formatted string containing status information + */ + public StatusReport statusReport() { + return new StatusReport(); + } + + /** + * Record a Throwable which will then be added to the status reporting text. + * + * @param t a Throwable to be reported (on demand) + */ + public void recordThrowable(Throwable t) { + stats.reportedThrowable = t; + } + + public void incrementInfoCount() { + stats.incrementEventCount("info", "total"); + } + +} diff --git a/src/main/java/org/onap/aai/validation/data/client/RestClient.java b/src/main/java/org/onap/aai/validation/data/client/RestClient.java new file mode 100644 index 0000000..d2d1196 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/data/client/RestClient.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.data.client; + +import com.sun.jersey.core.util.MultivaluedMapImpl; +import java.util.Arrays; +import java.util.UUID; +import javax.inject.Inject; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import org.onap.aai.restclient.client.OperationResult; +import org.onap.aai.validation.config.RestConfig; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; + +/** + * REST client capable of establishing secured requests. + */ +public class RestClient { + private RestConfig restConfig; + private org.onap.aai.restclient.client.RestClient aaiRestClient; + private MultivaluedMap<String, String> headers; + + private static final String ACCEPT = "application/json"; + private static final String HEADER_X_FROM_APP_ID = "validation-service"; + private static final String APP_CONFIG_HOME = System.getProperty("CONFIG_HOME"); + + /** + * Constructs a new rest client with the injected parameters. + * + * @param restConfig + */ + @Inject + public RestClient(RestConfig restConfig) { + this.restConfig = restConfig; + initialiseRestClient(); + } + + /** + * Initialises the REST client + * + */ + private void initialiseRestClient() { + // @formatter:off + aaiRestClient = new org.onap.aai.restclient.client.RestClient() + .validateServerHostname(false) + .validateServerCertChain(true) + .clientCertFile(APP_CONFIG_HOME + restConfig.getKeyStorePath()) + .clientCertPassword(restConfig.getKeyStorePassword()) + .trustStore(APP_CONFIG_HOME + restConfig.getTrustStorePath()) + .connectTimeoutMs(restConfig.getConnectionTimeout()) + .readTimeoutMs(restConfig.getReadTimeout()); + // @formatter:on + + headers = new MultivaluedMapImpl(); + headers.put("Accept", Arrays.asList(ACCEPT)); + headers.put("X-FromAppId", Arrays.asList(HEADER_X_FROM_APP_ID)); + headers.put("X-TransactionId", Arrays.asList(UUID.randomUUID().toString())); + } + + /** + * Invokes the REST URL and returns the payload string. + * + * @param uriPath + * @param mediaType + * @return The payload of the REST URL call as a string. + * @throws ValidationServiceException + */ + public String get(String uriPath, String mediaType) throws ValidationServiceException { + // Construct URI + String uri = restConfig.getProtocol() + "://" + restConfig.getHost() + ":" + restConfig.getPort() + uriPath; + + OperationResult result = aaiRestClient.get(uri, headers, MediaType.valueOf(mediaType)); + + if (result.getResultCode() == 200) { + return result.getResult(); + } else if (result.getResultCode() == 404) { + throw new ValidationServiceException(ValidationServiceError.REST_CLIENT_RESPONSE_NOT_FOUND, + result.getResultCode(), result.getFailureCause()); + } else { + throw new ValidationServiceException(ValidationServiceError.REST_CLIENT_RESPONSE_ERROR, + result.getResultCode(), result.getFailureCause()); + } + } + + /** + * POSTs a request. + * + * @param url + * @param payload + * + * @return The payload of the REST URL call as a string. + * @throws GapServiceException + */ + public String post(String url, String payload) throws ValidationServiceException { + OperationResult result = aaiRestClient.post(url, payload, headers, MediaType.APPLICATION_JSON_TYPE, + MediaType.APPLICATION_JSON_TYPE); + if (result.getResultCode() == 200) { + return result.getResult(); + } else { + throw new ValidationServiceException(ValidationServiceError.REST_CLIENT_RESPONSE_ERROR, + result.getResultCode(), result.getFailureCause()); + } + + } +} diff --git a/src/main/java/org/onap/aai/validation/exception/BaseValidationServiceException.java b/src/main/java/org/onap/aai/validation/exception/BaseValidationServiceException.java new file mode 100644 index 0000000..59fdb72 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/exception/BaseValidationServiceException.java @@ -0,0 +1,66 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.exception; + +import java.util.Locale; + +/** + * Validation service exception base class + * + */ +public class BaseValidationServiceException extends Exception { + + private static final long serialVersionUID = -6663403070792969748L; + + public static final Locale LOCALE = Locale.US; + + private final String id; + + /** + * Default constructor. + * + * @param id + */ + public BaseValidationServiceException(String id) { + super(); + this.id = id; + } + + /** + * @param id + * @param message + */ + public BaseValidationServiceException(String id, String message) { + super(message); + this.id = id; + } + + /** + * @param id + * @param message + * @param cause + */ + public BaseValidationServiceException(String id, String message, Throwable cause) { + super(message, cause); + this.id = id; + } + + public String getId() { + return this.id; + } +} diff --git a/src/main/java/org/onap/aai/validation/exception/ValidationServiceError.java b/src/main/java/org/onap/aai/validation/exception/ValidationServiceError.java new file mode 100644 index 0000000..45d79d4 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/exception/ValidationServiceError.java @@ -0,0 +1,103 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.exception; + +import java.text.MessageFormat; + +/** + * Error text formatting + * + */ +public enum ValidationServiceError { + + //@formatter:off + + // Rule Configuration exceptions. Range 100..199 + RULES_FILE_ERROR("VS-100", "Error reading rules configuration file(s) {0}"), + RULE_UNEXPECTED_TOKEN("VS-101", "Token {0} unexpected in rules configuration file."), + RULES_NOT_DEFINED("VS-102", "Event type {0} has no rule definitions."), + + // Rule exceptions. Range 200..299 + RULE_EXECUTION_ERROR("VS-201", "Error executing rule {0} with arguments {1}"), + + // Validation service processing exceptions. Range 300..399 + OMX_LOAD_ERROR("VS-300", "Validation service failed to load the OXM file."), + OXM_MISSING_KEY("VS-301", "Validation service failed to retrieve primary key for object of type {0}."), + MESSAGE_DIGEST_ERROR("VS-302", "Java platform security error. Failed to create Message Digest hashing object {0}."), + MODEL_RETRIEVAL_ERROR("VS-303", "Validator failed to get the model from external system."), + MODEL_CACHE_ERROR("VS-304", "Validator failed to retrieve the model from the cache. {0}"), + MODEL_NOT_FOUND("VS-305", "Model with UUID {0} not found."), + + // Event publishing exceptions. Range 400..499 + EVENT_CLIENT_PUBLISHER_INIT_ERROR("VS-400", "Error while initialising the Event Publisher Client."), + EVENT_CLIENT_SEND_ERROR("VS-401", "Error while sending a message to the event bus."), + EVENT_CLIENT_INCORRECT_NUMBER_OF_MESSAGES_SENT("VS-402", "Publisher client returned a result of {0} messages sent."), + EVENT_CLIENT_CLOSE_ERROR("VS-403", "Error while closing the Event Publisher Client."), + EVENT_CLIENT_CLOSE_UNSENT_MESSAGE("VS-404", "Failed to publish message. Error while closing the Event Publisher Client. " + + "The following message is unsent: {0}. Please check the logs for more information."), + EVENT_CLIENT_CONSUMER_INIT_ERROR("VS-405", "Error while initialising the Event Consumer Client."), + + // Reader exceptions. Range 500..599 + JSON_READER_PARSE_ERROR("VS-500", "JSON could not be parsed."), + EVENT_READER_MISSING_PROPERTY("VS-501", "Missing property: {0}"), + EVENT_READER_TOO_MANY_ENTITIES("VS-502", "Unexpected number or entities."), + INSTANCE_READER_NO_INSTANCE("VS-503", "Failed to extract instance under path: {0}. JSON payload: {1}"), + EVENT_READER_PROPERTY_READ_ERROR("VS-504", "Failed to read entity link property. Check event reader configuration properties."), + + // Model-instance mapping exceptions. Range 600..649 + MODEL_INSTANCE_MAPPING_RETRIEVAL_ERROR("VS-600", "Error retrieving model-instance mappings."), + MODEL_INSTANCE_MAPPING_FILE_IO_ERROR("VS-601", "IO error when reading from the model-instance mapping file {0}."), + MODEL_PARSE_ERROR("VS-602", "Validator failed to parse the model provided. Source:\n{0}"), + MODEL_VALUE_ERROR("VS-603", "Validator failed to access the model value {0} in {1}"), + INSTANCE_MAPPING_ROOT_ERROR("VS-604", "Missing 'root' property in instance mapping."), + + // REST client exceptions. Range 650..699 + REST_CLIENT_RESPONSE_ERROR("VS-650", "REST client response error: Response Code: {0}, Failure Cause: {1}."), + REST_CLIENT_RESPONSE_NOT_FOUND("VS-651", "REST client response error: Response Code: {0}, Failure Cause: {1}."), + + // Validation Service configuration exceptions. Range 700..799 + VS_PROPERTIES_LOAD_ERROR("VS-700", "Failed to read property {0} in the validation service properties file."), + + // Miscellaneous exceptions. Range 800..849 + STRING_UTILS_INVALID_REGEX("VS-800", "Invalid regular expression: {0}"); + + //@formatter:on + + private String id; + private String message; + + private ValidationServiceError(String id, String message) { + this.id = id; + this.message = message; + } + + public String getId() { + return this.id; + } + + /** + * @param args + * to be formatted + * @return the formatted error message + */ + public String getMessage(Object... args) { + MessageFormat formatter = new MessageFormat(""); + formatter.applyPattern(this.message); + return formatter.format(args); + } +} diff --git a/src/main/java/org/onap/aai/validation/exception/ValidationServiceException.java b/src/main/java/org/onap/aai/validation/exception/ValidationServiceException.java new file mode 100644 index 0000000..2606ce9 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/exception/ValidationServiceException.java @@ -0,0 +1,78 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.exception; + +/** + * Validation service exception + * + */ +public class ValidationServiceException extends BaseValidationServiceException { // NOSONAR + + private static final long serialVersionUID = 883498159309797607L; + + /** + * Constructs an exception defined by the Error. + * + * @param error + * {@link ValidationServiceError} with the error id. + */ + public ValidationServiceException(ValidationServiceError error) { + super(error.getId(), error.getId() + ", " + error.getMessage()); + } + + /** + * Constructs an exception defined by the Error. The message is parameterised with the arguments. + * + * @param error + * {@link ValidationServiceError} with the error id. + * @param args + * Arguments for the exception message. + */ + public ValidationServiceException(ValidationServiceError error, Object... args) { + super(error.getId(), error.getId() + ", " + error.getMessage(args)); + } + + /** + * Constructs an exception defined by the Error and the underlying Exception. The message is parameterised with the + * arguments and enhanced with the underlying Exception message. + * + * @param error + * {@link ValidationServiceError} with the error id. + * @param exception + * Exception thrown by an underlying API. + * @param args + * Arguments for the exception message. + */ + public ValidationServiceException(ValidationServiceError error, Exception exception, Object... args) { + super(error.getId(), error.getId() + ", " + error.getMessage(args) + "; Caused by: " + exception.getMessage(), exception); + } + + /** + * Constructs an exception defined by the Error and the underlying Exception. The message is enhanced with the + * underlying Exception message. + * + * @param error + * {@link ValidationServiceError} with the error id. + * @param exception + * Exception thrown by an underlying API. + */ + public ValidationServiceException(ValidationServiceError error, Exception exception) { + super(error.getId(), error.getId() + ", " + error.getMessage() + "; Caused by: " + exception.getMessage(), exception); + } + +} diff --git a/src/main/java/org/onap/aai/validation/factory/DMaaPEventPublisherFactory.java b/src/main/java/org/onap/aai/validation/factory/DMaaPEventPublisherFactory.java new file mode 100644 index 0000000..fa4af74 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/factory/DMaaPEventPublisherFactory.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.factory; + +import org.onap.aai.event.client.DMaaPEventPublisher; + +public class DMaaPEventPublisherFactory { + + + public DMaaPEventPublisher createEventPublisher(String topicHost, String topicName, String topicUsername, + String topicPassword, String topicTransportType) { + int defaultBatchSize = DMaaPEventPublisher.DEFAULT_BATCH_SIZE; + long defaultBatchAge = DMaaPEventPublisher.DEFAULT_BATCH_AGE; + int defaultBatchDelay = DMaaPEventPublisher.DEFAULT_BATCH_DELAY; + return new DMaaPEventPublisher(topicHost, topicName, topicUsername, topicPassword, defaultBatchSize, + defaultBatchAge, defaultBatchDelay, topicTransportType); + } + +} diff --git a/src/main/java/org/onap/aai/validation/logging/ApplicationMsgs.java b/src/main/java/org/onap/aai/validation/logging/ApplicationMsgs.java new file mode 100644 index 0000000..6066a20 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/logging/ApplicationMsgs.java @@ -0,0 +1,74 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.logging; + +import com.att.eelf.i18n.EELFResourceManager; +import org.onap.aai.cl.eelf.LogMessageEnum; + +/** + * Application messages + */ +public enum ApplicationMsgs implements LogMessageEnum { + + /** + * Add message keys here. + */ + // @formatter:off + MESSAGE_VALIDATION_REQUEST, + MESSAGE_AUDIT, + MESSAGE_METRIC, + MESSAGE_PUBLISH_ERROR, + OXM_LOAD_ERROR, + OXM_MISSING_KEY_ERROR, + MISSING_REQUEST_ID, + CANNOT_VALIDATE_ERROR, + CANNOT_VALIDATE_HANDLE_EXCEPTION_ERROR, + POLL_EVENTS, + NUMBER_OF_MESSAGES_CONSUMED, + INVOKE_EVENT_CONSUMER_ERROR, + READ_FILE_ERROR, + STARTUP_SERVLET_INIT, + POLLING_INTERVAL_CONFIG_NOT_PRESENT, + POLLING_FOR_EVENTS, + POLLING_DISABLED, + STARTUP_SERVLET_INIT_SUCCESS, + UNSENT_MESSAGE_WARN, + UNSENT_MESSAGE_ERROR, + EVENT_CLIENT_CLOSE_UNSENT_MESSAGE, + SEND_MESSAGE_ABORT_WARN, + SEND_MESSAGE_RETRY_WARN, + FILE_ARG_NULL_ERROR, + LOAD_PROPERTIES, + FILE_LOAD_INTO_MAP, + FILE_LOAD_INTO_MAP_ERROR, + CREATE_PROPERTY_MAP_ERROR, + FILE_MONITOR_BLOCK_ERROR, + READ_FILE_STREAM_ERROR, + STRING_UTILS_INVALID_REGEX, + MALFORMED_REQUEST_ERROR, + PROCESS_REQUEST_ERROR; + // @formatter:on + + /** + * Static initializer to ensure the resource bundles for this class are loaded... Here this application loads + * messages from three bundles + */ + static { + EELFResourceManager.loadMessageBundle("validation-service-logging-resources"); + } +} diff --git a/src/main/java/org/onap/aai/validation/logging/LogHelper.java b/src/main/java/org/onap/aai/validation/logging/LogHelper.java new file mode 100644 index 0000000..4abf5fd --- /dev/null +++ b/src/main/java/org/onap/aai/validation/logging/LogHelper.java @@ -0,0 +1,548 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.logging; + +import static com.att.eelf.configuration.Configuration.MDC_SERVICE_NAME; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.core.FileAppender; +import com.att.eelf.configuration.EELFLogger; +import com.att.eelf.configuration.EELFManager; +import com.att.eelf.i18n.EELFResolvableErrorEnum; +import java.io.File; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import javax.servlet.ServletRequest; +import javax.ws.rs.core.Response.Status; +import org.apache.commons.lang.time.StopWatch; +import org.onap.aai.cl.api.LogFields; +import org.onap.aai.cl.api.Logger; +import org.onap.aai.cl.mdc.MdcContext; +import org.onap.aai.cl.mdc.MdcOverride; +import org.onap.aai.restclient.client.Headers; +import org.onap.aai.validation.services.RequestHeaders; +import org.slf4j.MDC; +import org.springframework.http.HttpHeaders; + +/*- + * This Log Helper mimics the interface of a Common Logging Logger + * but adds helper methods for audit and metrics logging requirements. + * + * Messages are logged to the appropriate EELF functional logger as described below. + * + * Error Log: INFO/WARN/ERROR/FATAL + * Debug Log: DEBUG/TRACE + * Audit Log: summary view of transaction processing + * Metrics Log: detailed timings of transaction processing interactions + * + * Audit and Metrics log messages record the following fields: + * + * RequestID - an RFC4122 UUID for the transaction request + * ServiceName - the API provided by this service + * PartnerName - invoker of the API + * ClassName - name of the class creating the log record + * + * The above list is not exhaustive. + */ +public enum LogHelper implements Logger { + INSTANCE; // Joshua Bloch's singleton pattern + + @FunctionalInterface + public interface TriConsumer<T, U, V> { + public void accept(T t, U u, V v); + } + + /** + * Audit log message status code values. See {@code MdcParameter.STATUS_CODE} + */ + public enum StatusCode { + COMPLETE, + ERROR; + } + + /** + * Mapped Diagnostic Context parameter names. + * + * Note that MdcContext.MDC_START_TIME is used for audit messages, and indicates the start of a transaction. + * Messages in the metrics log record sub-operations of a transaction and thus use different timestamps. + */ + public enum MdcParameter { + REQUEST_ID(MdcContext.MDC_REQUEST_ID), + CLASS_NAME("ClassName"), + BEGIN_TIMESTAMP("BeginTimestamp"), + END_TIMESTAMP("EndTimestamp"), + ELAPSED_TIME("ElapsedTime"), + STATUS_CODE("StatusCode"), + RESPONSE_CODE("ResponseCode"), + RESPONSE_DESCRIPTION("ResponseDescription"), + TARGET_ENTITY("TargetEntity"), + TARGET_SERVICE_NAME("TargetServiceName"), + USER("User"); + + private final String parameterName; + + MdcParameter(String parameterName) { + this.parameterName = parameterName; + } + + /** + * Get the MDC logging context parameter name as referenced by the logback configuration + * + * @return the MDC parameter name + */ + public String value() { + return parameterName; + } + } + + /** + * Our externally advertised service API + */ + private static final String SERVICE_NAME_VALUE = "AAI-VS"; + + private static final EELFLogger errorLogger = EELFManager.getInstance().getErrorLogger(); + private static final EELFLogger debugLogger = EELFManager.getInstance().getDebugLogger(); + private static final EELFLogger auditLogger = EELFManager.getInstance().getAuditLogger(); + private static final EELFLogger metricsLogger = EELFManager.getInstance().getMetricsLogger(); + + /** + * Formatting for timestamps logged as Strings (from the MDC) + */ + private DateFormat timestampFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + + // Records the elapsed time (since the start of servicing a request) for audit logging + private StopWatch auditStopwatch; + + /** + * Initialises the MDC (logging context) with default values, to support any logging of messages BEFORE the + * startAudit() method is invoked. + */ + private LogHelper() { + setContextValue(MDC_SERVICE_NAME, SERVICE_NAME_VALUE); + // This value is not expected to be used in the default logging configuration + setContextValue(MdcContext.MDC_START_TIME, timestampFormat.format(new Date())); + } + + /** + * Begin recording transaction information for a new request. This data is intended for logging purposes. This + * method does not actually write any messages to the log(s). + * + * Initialise the MDC logging context for auditing and metrics, using the HTTP request headers. This information + * includes: the correlation ID, local application service name/ID, calling host details and authentication data. + * + * The request object is used to find the client details (e.g. IP address) + * + * @param headers raw HTTP headers + * @param servletRequest the request + */ + public void startAudit(final HttpHeaders headers, ServletRequest servletRequest) { + auditStopwatch = new StopWatch(); + auditStopwatch.start(); + + String requestId = null; + String serviceInstanceId = null; + String partnerName = "<UNKNOWN_PARTNER>"; + + if (headers != null) { + requestId = setRequestId(headers); + serviceInstanceId = headers.getFirst(RequestHeaders.HEADER_SERVICE_INSTANCE_ID); + partnerName = headers.getFirst(Headers.FROM_APP_ID); + } + + String clientHost = null; + String clientIPAddress = null; + String user = "<UNKNOWN_USER>"; + + if (servletRequest != null) { + clientHost = servletRequest.getRemoteHost(); + clientIPAddress = servletRequest.getRemoteAddr(); + + if (partnerName == null) { + partnerName = clientHost; + } + } + + // Populate standard MDC keys - note that the initialize method calls MDC.clear() + MdcContext.initialize(requestId, SERVICE_NAME_VALUE, serviceInstanceId, partnerName, clientIPAddress); + + setContextValue(MdcParameter.USER, user); + setContextValue(MdcContext.MDC_REMOTE_HOST, clientHost); + } + + /** + * Store a value in the current thread's logging context. + * + * @param key non-null parameter name + * @param value the value to store against the key + */ + public void setContextValue(String key, String value) { + debug(key + "=" + value); + MDC.put(key, value); + } + + /** + * Store a value in the current thread's logging context. + * + * @param param identifier of the context parameter + * @param value the value to store for this parameter + */ + public void setContextValue(MdcParameter param, String value) { + setContextValue(param.value(), value); + } + + /** + * Set a value in the current thread's logging context, only if this is not already set. + * + * @param key non-null parameter name + * @param value the value to store against the key (only if the current value is null) + */ + public void setDefaultContextValue(String key, String value) { + if (MDC.get(key) == null) { + setContextValue(key, value); + } + } + + /** + * Set a value in the current thread's logging context, only if this is not already set. + * + * @param param identifier of the context parameter + * @param value the value to store for this parameter (only if the current value is null) + */ + public void setDefaultContextValue(MdcParameter param, String value) { + setContextValue(param.value(), value); + } + + /** + * Clear all logging context values. This should be called at start-up only. + */ + public void clearContext() { + debug("Clearing MDC context"); + MDC.clear(); + } + + /** + * Clear the specified logging context value. + */ + public void clearContextValue(MdcParameter param) { + MDC.remove(param.value()); + } + + /** + * Log an audit message to the audit logger. This method is expected to be called when a response is returned to the + * caller and/or when the processing of the request completes. + * + * @param status status of the service request: COMPLETE/ERROR + * @param responseCode optional application error code + * @param responseDescription human-readable description of the response code + * @param args the argument(s) required to populate the Audit Message log content + */ + public void logAudit(StatusCode status, String responseCode, String responseDescription, final String... args) { + if (auditStopwatch == null) { + debug("Unexpected program state: audit stopwatch not started"); + auditStopwatch = new StopWatch(); + auditStopwatch.start(); + } + + if (auditLogger.isInfoEnabled()) { + setMdcElapsedTime(auditStopwatch); + setContextValue(MdcParameter.STATUS_CODE, status.toString()); + setContextValue(MdcParameter.RESPONSE_CODE, responseCode); + setContextValue(MdcParameter.RESPONSE_DESCRIPTION, responseDescription); + invokeLogger(auditLogger::info, ApplicationMsgs.MESSAGE_AUDIT, args); + } + } + + /** + * Log an audit message to the audit logger representing a non-specific processing success message. + * + * @param msg + */ + public void logAuditSuccess(String msg) { + Status status = Status.OK; + logAudit(StatusCode.COMPLETE, Integer.toString(status.getStatusCode()), status.getReasonPhrase(), msg); + } + + /** + * Log an audit message to the audit logger representing an internal error (e.g. for an exception thrown by the + * implementation). This method is expected to be called when a generic error response is returned to the caller to + * indicate a processing failure. + * + * @param e Exception + */ + public void logAuditError(Exception e) { + Status status = Status.INTERNAL_SERVER_ERROR; + logAudit(StatusCode.ERROR, Integer.toString(status.getStatusCode()), status.getReasonPhrase(), e.getMessage()); + } + + /** + * Log a message to the metrics log. + * + * @param error the log code + * @param args the info messages + */ + public void logMetrics(final String... args) { + if (metricsLogger.isInfoEnabled()) { + invokeLogger(metricsLogger::info, ApplicationMsgs.MESSAGE_METRIC, args); + } + } + + /** + * @param stopwatch + * @param args + */ + public void logMetrics(final StopWatch stopwatch, String... args) { + setMdcElapsedTime(stopwatch); + logMetrics(args); + } + + @Override + public String formatMsg(@SuppressWarnings("rawtypes") Enum arg0, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDebugEnabled() { + return debugLogger != null && debugLogger.isDebugEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return errorLogger.isErrorEnabled(); + } + + @Override + public boolean isInfoEnabled() { + return errorLogger.isInfoEnabled(); + } + + @Override + public boolean isTraceEnabled() { + return debugLogger.isTraceEnabled(); + } + + @Override + public boolean isWarnEnabled() { + return errorLogger.isWarnEnabled(); + } + + /** + * Log a DEBUG level message to the debug logger. + * + * @param message the debug message + */ + @Override + public void debug(String message) { + if (isDebugEnabled()) { + invokeLogger(debugLogger::debug, message); + } + } + + @Override + public void debug(@SuppressWarnings("rawtypes") Enum errorCode, String... args) { + if (isDebugEnabled()) { + invokeErrorCodeLogger(debugLogger::debug, (EELFResolvableErrorEnum) errorCode, args); + } + } + + @Override + public void debug(@SuppressWarnings("rawtypes") Enum errorCode, LogFields arg1, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public void error(@SuppressWarnings("rawtypes") Enum errorCode, String... args) { + if (isErrorEnabled()) { + invokeErrorCodeLogger(errorLogger::error, (EELFResolvableErrorEnum) errorCode, args); + } + } + + @Override + public void error(@SuppressWarnings("rawtypes") Enum errorCode, Throwable t, String... args) { + if (isErrorEnabled()) { + invokeErrorCodeLogger(errorLogger::error, (EELFResolvableErrorEnum) errorCode, t, args); + } + } + + @Override + public void error(@SuppressWarnings("rawtypes") Enum errorCode, LogFields arg1, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public void error(@SuppressWarnings("rawtypes") Enum errorCode, LogFields arg1, Throwable arg2, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public void info(@SuppressWarnings("rawtypes") Enum errorCode, String... args) { + if (isInfoEnabled()) { + invokeErrorCodeLogger(errorLogger::info, (EELFResolvableErrorEnum) errorCode, args); + } + } + + @Override + public void info(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public void info(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, MdcOverride arg2, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public void trace(@SuppressWarnings("rawtypes") Enum errorCode, String... args) { + if (isTraceEnabled()) { + invokeErrorCodeLogger(debugLogger::trace, (EELFResolvableErrorEnum) errorCode, args); + } + } + + @Override + public void trace(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public void warn(@SuppressWarnings("rawtypes") Enum errorCode, String... args) { + if (isWarnEnabled()) { + invokeErrorCodeLogger(errorLogger::warn, (EELFResolvableErrorEnum) errorCode, args); + } + } + + @Override + public void warn(@SuppressWarnings("rawtypes") Enum arg0, LogFields arg1, String... args) { + throw new UnsupportedOperationException(); + } + + /** + * Get the method name for a calling method (from the current stack trace) + * + * @param level number of levels for the caller (not including this method) + * @return the class and name of the calling method in the form "class#method" + */ + public static String getCallerMethodName(int level) { + StackTraceElement callingMethod = Thread.currentThread().getStackTrace()[level + 2]; + return callingMethod.getClassName() + "#" + callingMethod.getMethodName(); + } + + + /** + * Convenience method to be used only for testing purposes. + * + * @return the directory storing the log files + */ + public static String getLogDirectory() { + ch.qos.logback.classic.Logger logger = + (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger("com.att.eelf"); + AsyncAppender appender = (AsyncAppender) logger.getAppender("asyncEELF"); + FileAppender<?> fileAppender = (FileAppender<?>) appender.getAppender("EELF"); + return new File(fileAppender.getFile()).getParent(); + } + + /** + * @param headers + * @return the request ID from the HTTP headers + */ + private String setRequestId(final HttpHeaders headers) { + String requestId = headers.getFirst(RequestHeaders.HEADER_REQUEST_ID); + + // If the request ID is missing, set it from the transaction ID + if (requestId == null) { + requestId = headers.getFirst(Headers.TRANSACTION_ID); + } + + // Normalize the incoming ID by removing any suffix + if (requestId != null && requestId.contains(":")) { + requestId = requestId.split(":")[0]; + } + + return requestId == null ? "missing-request-id" : requestId; + } + + private void setMdcClassName() { + MDC.put(MdcParameter.CLASS_NAME.value(), getCallerMethodName(3)); + } + + private void unsetMdcClassName() { + MDC.put(MdcParameter.CLASS_NAME.value(), null); + } + + /** + * Set the Begin, End, and Elapsed time values in the MDC context. + * + * @param stopwatch + */ + private void setMdcElapsedTime(final StopWatch stopwatch) { + long startTime = stopwatch.getStartTime(); + long elapsedTime = stopwatch.getTime(); + + setContextValue(MdcParameter.BEGIN_TIMESTAMP, timestampFormat.format(startTime)); + setContextValue(MdcParameter.END_TIMESTAMP, timestampFormat.format(startTime + elapsedTime)); + setContextValue(MdcParameter.ELAPSED_TIME, Long.toString(elapsedTime)); // Milliseconds + } + + /** + * @param logMethod the logger method to invoke + * @param message + */ + private void invokeLogger(Consumer<String> logMethod, String message) { + setMdcClassName(); + logMethod.accept(message); + unsetMdcClassName(); + } + + /** + * @param logMethod + * @param msg + * @param args + */ + private void invokeLogger(BiConsumer<ApplicationMsgs, String[]> logMethod, ApplicationMsgs msg, String[] args) { + setMdcClassName(); + logMethod.accept(msg, args); + unsetMdcClassName(); + } + + /** + * @param logMethod + * @param errorEnum + * @param args + */ + private void invokeErrorCodeLogger(BiConsumer<EELFResolvableErrorEnum, String[]> logMethod, + EELFResolvableErrorEnum errorEnum, String[] args) { + setMdcClassName(); + logMethod.accept(errorEnum, args); + unsetMdcClassName(); + } + + /** + * @param logMethod + * @param errorEnum + * @param t a Throwable + * @param args + */ + private void invokeErrorCodeLogger(TriConsumer<EELFResolvableErrorEnum, Throwable, String[]> logMethod, + EELFResolvableErrorEnum errorEnum, Throwable t, String[] args) { + setMdcClassName(); + logMethod.accept(errorEnum, t, args); + unsetMdcClassName(); + } + +} diff --git a/src/main/java/org/onap/aai/validation/modeldriven/ModelCacheManager.java b/src/main/java/org/onap/aai/validation/modeldriven/ModelCacheManager.java new file mode 100644 index 0000000..5c741fb --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/ModelCacheManager.java @@ -0,0 +1,182 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.ws.rs.core.MediaType; +import org.apache.http.client.utils.URIBuilder; +import org.dom4j.Element; +import org.onap.aai.validation.config.ModelConfig; +import org.onap.aai.validation.config.RestConfig; +import org.onap.aai.validation.data.client.RestClient; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.modeldriven.parser.XMLModelParser; + +/** + * A cache of model UUID against the parsed model Element, using Guava's CacheBuilder. + */ +public class ModelCacheManager { + + public static final String FILE_MODEL_PROTOCOL = "file"; + + private LoadingCache<ModelId, Element> modelCache; + private RestConfig restConfig; + + /** + * Initialises the instance by loading validator properties from config. + * + * @param modelConfig + * @param restConfig + * @throws ValidationServiceException + */ + @Inject + public ModelCacheManager(ModelConfig modelConfig, RestConfig restConfig) { + this.restConfig = restConfig; + + // Create an expiring cache with a load implementation which is executed when a key value is not cached. + modelCache = CacheBuilder.newBuilder().maximumSize(1000) + .expireAfterWrite(modelConfig.getModelCacheExpirySeconds(), TimeUnit.SECONDS) + .build(new CacheLoader<ModelId, Element>() { + @Override + public Element load(ModelId key) throws ValidationServiceException { + return retrieveModelElement(key); + } + }); + } + + /** + * Gets the model with specified uuid from the model cache. If model is not cached, retrieve the model from the + * external system. + * + * @param uuid + * The model UUID. + * @return The DOM Element representing the model's root element. + * @throws ValidationServiceException + */ + public Element get(ModelId uuid) throws ValidationServiceException { + if (uuid == null || uuid.isEmpty()) { + return null; + } + + return getCachedModel(uuid); + } + + private Element getCachedModel(ModelId uuid) throws ValidationServiceException { + Element element = null; + try { + element = modelCache.get(uuid); + } catch (ExecutionException e) { + // If the wrapped exception is a model validation error, return null. + Throwable cause = e.getCause(); + if (cause != null && cause.getClass().equals(ValidationServiceException.class) + && (((ValidationServiceException) cause).getId().equals(ValidationServiceError.MODEL_NOT_FOUND.getId()) + || ((ValidationServiceException) cause).getId().equals(ValidationServiceError.REST_CLIENT_RESPONSE_NOT_FOUND.getId()))) { + return null; + } + throw new ValidationServiceException(ValidationServiceError.MODEL_CACHE_ERROR, e, ""); + } + return element; + } + + /** + * Puts an item into the model cache. + * + * @param uuid + * The key. + * @param modelElement + * The value. + */ + public void put(ModelId uuid, Element modelElement) { + modelCache.put(uuid, modelElement); + } + + /** + * Constructs and invokes the configured REST URL. The XML payload is then parsed into a model Element. + * + * @param uuid + * The model UUID to retrieve. + * @return The payload of the REST URL call as a model Element. + * @throws ValidationServiceException + * if the payload is null + */ + private Element retrieveModelElement(ModelId uuid) throws ValidationServiceException { + String protocol = restConfig.getProtocol(); + Element modelElement; + if (FILE_MODEL_PROTOCOL.equals(protocol)) { + File modelFile = new File(restConfig.getBaseModelURI()); + modelElement = retrieveModelElementFromFile(modelFile, uuid); + } else { + modelElement = retrieveModelElementFromREST(restConfig, uuid); + } + + // Do not store a null value in the CacheBuilder + if (modelElement != null) { + return modelElement; + } else { + throw new ValidationServiceException(ValidationServiceError.MODEL_NOT_FOUND, uuid); + } + } + + /** + * Constructs and invokes the configured REST URL to retrieve the model for the supplied UUID. The XML payload is + * then parsed into a model Element. + * + * @param uuid + * The model UUID to retrieve. + * @return The payload of the REST URL call as a model Element, or null of no model could be found. + * @throws ValidationServiceException + */ + private Element retrieveModelElementFromREST(RestConfig restConfig, ModelId uuid) + throws ValidationServiceException { + try { + URI restURI = new URIBuilder(restConfig.getBaseModelURI()) + .addParameter(uuid.getModelIdAttribute(), uuid.getModelId()).build(); + String restPayload = new RestClient(restConfig).get(restURI.toString(), MediaType.APPLICATION_XML); + return XMLModelParser.parse(restPayload, true); + } catch (URISyntaxException e) { + throw new ValidationServiceException(ValidationServiceError.MODEL_RETRIEVAL_ERROR, e); + } + } + + /** + * Retrieves the model with supplied model UUID from the supplied model file. The XML payload is then parsed into a + * model Element. + * + * @param uuid + * The model UUID to retrieve. + * @return The model Element, or null if no model could be found. + * @throws ValidationServiceException + */ + private Element retrieveModelElementFromFile(File modelFile, ModelId uuid) throws ValidationServiceException { + Element rootElement = XMLModelParser.parse(modelFile, false); + // Check that the root element of the model file is correct. + if (!XMLModelParser.MODELS_ROOT_ELEMENT.equals(rootElement.getName())) { + return null; + } + return XMLModelParser.getModelElementWithId(rootElement, uuid.getModelIdAttribute(), uuid.getModelId()); + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/modeldriven/ModelId.java b/src/main/java/org/onap/aai/validation/modeldriven/ModelId.java new file mode 100644 index 0000000..25fbc22 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/ModelId.java @@ -0,0 +1,86 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven; + +import java.util.Objects; +import org.apache.commons.lang3.builder.EqualsBuilder; + +/** + * Bean representing a model ID. + * + * The modelIdAttribute field defines the attribute in the model that holds the modelId + */ +public class ModelId { + + public static final String ATTR_MODEL_NAME_VERSION_ID = "model-name-version-id"; + public static final String ATTR_MODEL_ID = "model-id"; + + private String modelIdAttribute; + private String id; + + /** + * @param modelIdAttribute The name of the attribute that holds the model ID. + * @param modelId The model ID. + */ + public ModelId(String modelIdAttribute, String modelId) { + super(); + this.modelIdAttribute = modelIdAttribute; + this.id = modelId; + } + + public String getModelIdAttribute() { + return modelIdAttribute; + } + + public void setModelIdAttribute(String modelIdAttribute) { + this.modelIdAttribute = modelIdAttribute; + } + + public String getModelId() { + return id; + } + + public void setModelId(String modelId) { + this.id = modelId; + } + + public boolean isEmpty() { + return modelIdAttribute == null || id == null || modelIdAttribute.isEmpty() || id.isEmpty(); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.modelIdAttribute); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ModelId)) { + return false; + } else if (obj == this) { + return true; + } + ModelId rhs = (ModelId) obj; + // @formatter:off + return new EqualsBuilder() + .append(id, rhs.id) + .append(modelIdAttribute, rhs.modelIdAttribute) + .isEquals(); + // @formatter:on + } +} diff --git a/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/Filter.java b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/Filter.java new file mode 100644 index 0000000..a251d54 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/Filter.java @@ -0,0 +1,75 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.configuration.mapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apache.commons.lang3.builder.EqualsBuilder; + +/** + * Defines a path and value that will be used to validate a particular model. + * @see ValueConfiguration and ModelInstanceMapper + */ +public class Filter { + + private String path; + private List<String> valid = new ArrayList<>(); + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List<String> getValid() { + return valid; + } + + public void setValid(List<String> valid) { + this.valid = valid; + } + + @Override + public int hashCode() { + return Objects.hash(path, valid); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Filter)) { + return false; + } else if (obj == this) { + return true; + } + Filter rhs = (Filter) obj; + // @formatter:off + return new EqualsBuilder() + .append(path, rhs.path) + .append(valid, rhs.valid) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return "Filter [path=" + path + ", valid=" + valid + "]"; + } +} diff --git a/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ModelInstanceMapper.java b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ModelInstanceMapper.java new file mode 100644 index 0000000..9ebe03c --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ModelInstanceMapper.java @@ -0,0 +1,89 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.configuration.mapping; + +import java.util.Objects; +import org.apache.commons.lang3.builder.EqualsBuilder; + +/** + * Maps the configuration of model and instance values to be compared. + */ +public class ModelInstanceMapper { + + /** + * Types of mappings. + */ + public enum MappingType { + RELATIONSHIP, ATTRIBUTE + } + + private MappingType mappingType; + private ValueConfiguration model; + private ValueConfiguration instance; + + public MappingType getMappingType() { + return mappingType; + } + + public void setMappingType(String mappingType) { + this.mappingType = MappingType.valueOf(mappingType); + } + + public ValueConfiguration getModel() { + return model; + } + + public void setModel(ValueConfiguration model) { + this.model = model; + } + + public ValueConfiguration getInstance() { + return instance; + } + + public void setInstance(ValueConfiguration instance) { + this.instance = instance; + } + + @Override + public int hashCode() { + return Objects.hash(instance, mappingType, model); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ModelInstanceMapper)) { + return false; + } else if (obj == this) { + return true; + } + ModelInstanceMapper rhs = (ModelInstanceMapper) obj; + // @formatter:off + return new EqualsBuilder() + .append(instance, rhs.instance) + .append(mappingType, rhs.mappingType) + .append(model, rhs.model) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return "ModelInstanceMapper [mappingType=" + mappingType + ", model=" + model + ", instance=" + instance + "]"; + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ModelInstanceMappingReader.java b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ModelInstanceMappingReader.java new file mode 100644 index 0000000..7be2d76 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ModelInstanceMappingReader.java @@ -0,0 +1,69 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.configuration.mapping; + +import java.util.ArrayList; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.util.JsonUtil; + +/** + * Read mapping files + */ +public class ModelInstanceMappingReader { + + private String mappingFile; + + /** + * @param mappingFile + */ + public ModelInstanceMappingReader(String mappingFile) { + this.mappingFile = mappingFile; + } + + /** + * Returns a list of model and object instance paths that will be used for comparing the corresponding model and + * instance elements. The mappings are defined in the configuration file model-instance-mapping.json_conf and follows the + * JSON notation. + * + * @return a List of {@link ModelInstanceMapper} beans which represents all the mappings defined in the file + * model-instance-mapping.json_conf. + * @throws ValidationServiceException + */ + public List<ModelInstanceMapper> getMappings() throws ValidationServiceException { + List<ModelInstanceMapper> mappings = new ArrayList<>(); + + try { + JSONArray mappingsArray = new JSONArray(mappingFile); + + for (int i = 0; i < mappingsArray.length(); i++) { + JSONObject jsonObject = mappingsArray.getJSONObject(i); + ModelInstanceMapper mapping = JsonUtil.fromJson(jsonObject.toString(), ModelInstanceMapper.class); + mappings.add(mapping); + } + } catch (JSONException e) { + throw new ValidationServiceException(ValidationServiceError.MODEL_INSTANCE_MAPPING_RETRIEVAL_ERROR, e); + } + + return mappings; + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ValueConfiguration.java b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ValueConfiguration.java new file mode 100644 index 0000000..230bbd9 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/configuration/mapping/ValueConfiguration.java @@ -0,0 +1,124 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.configuration.mapping; + +import java.util.Objects; +import org.apache.commons.lang3.builder.EqualsBuilder; + +/** + * Describes a model or instance value that is used in model to instance comparison. + * + * @see ModelInstanceMapper + */ +public class ValueConfiguration { + + /** Top level element. From which all navigation starts. */ + private String origin; + + /** + * Root element from which the value will be extracted. Can be used to recursively navigate the model/instance + * hierarchy. + */ + private String root; + + /** + * Provides validation on the model before the value is extracted. If the filter is not satisfied other root models + * will be searched. + */ + private Filter filter; + + /** + * Path to the model id + */ + private String id; + + /** + * Path to a model or instance value + */ + private String value; + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public String getRoot() { + return root; + } + + public void setRoot(String root) { + this.root = root; + } + + public Filter getFilter() { + return filter; + } + + public void setFilter(Filter filter) { + this.filter = filter; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public int hashCode() { + return Objects.hash(this.filter, this.origin, this.root, this.value, this.id); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ValueConfiguration)) { + return false; + } else if (obj == this) { + return true; + } + ValueConfiguration rhs = (ValueConfiguration) obj; + // @formatter:off + return new EqualsBuilder() + .append(filter, rhs.filter) + .append(origin, rhs.origin) + .append(root, rhs.root) + .append(value, rhs.value) + .append(id, rhs.id) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return "ValueConfiguration [origin=" + origin + ", root=" + root + ", id=" + id + ", filter=" + filter + + ", value=" + value + "]"; + } +} diff --git a/src/main/java/org/onap/aai/validation/modeldriven/parser/XMLModelParser.java b/src/main/java/org/onap/aai/validation/modeldriven/parser/XMLModelParser.java new file mode 100644 index 0000000..cf7aa38 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/parser/XMLModelParser.java @@ -0,0 +1,152 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.parser; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.List; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.Node; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; + +/** + * Read models from XML file + * + */ +public class XMLModelParser { + + public static final String MODEL_ROOT_ELEMENT = "model"; + public static final String MODELS_ROOT_ELEMENT = "models"; + + private XMLModelParser() { + // Do nothing + } + + /** + * Parses an xml file and returns the root Element. + * + * @param modelFile + * The XML File. + * @param validateModel + * If true the model will be validated. + * @return The root Element of the document. + * @throws DocumentException + * @throws ValidationServiceException + */ + public static Element parse(File modelFile, boolean validateModel) throws ValidationServiceException { + try { + byte[] encoded = Files.readAllBytes(modelFile.toPath()); + String modelString = new String(encoded, Charset.defaultCharset()); + + return parse(modelString, validateModel); + } catch (IOException e) { + throw new ValidationServiceException(ValidationServiceError.MODEL_PARSE_ERROR, e, modelFile.toString()); + } + } + + /** + * Parses an xml String and returns the root Element. + * + * @param modelString + * The XML String. + * @param validateModel + * If true the model will be validated. + * @return The root Element of the document, or null if no valid model can be parsed. + * @throws DocumentException + */ + public static Element parse(String modelString, boolean validateModel) throws ValidationServiceException { + // Strip namespace information first. + String model = removeXmlStringNamespaceAndPreamble(modelString); + + try { + Element modelElement = DocumentHelper.parseText(model).getRootElement(); + if (validateModel && (modelElement == null || !isValidModel(modelElement))) { + return null; + } + return modelElement; + } catch (DocumentException e) { + throw new ValidationServiceException(ValidationServiceError.MODEL_PARSE_ERROR, e, model); + } + } + + /** + * Returns the result of running the XPath expression on the DOM Node. + * + * @param currentNode + * The current Node being processed. + * @param xPath + * The XPath expression to run on the Node. + * @return A List of Nodes representing the result of the XPath expression. + */ + @SuppressWarnings("unchecked") + public static List<Node> getObjectsFromXPath(Node currentNode, String xPath) { + return currentNode.selectNodes(xPath); + } + + /** + * Returns the child model Element that corresponds to the provided root Element and model ID. + * + * @param rootElement + * The root Element to search. + * @param attributeName + * The name of the attribute that holds the model ID. + * @param modelId + * The model ID to search for. + * @return The model Element that matches the model ID supplied. + */ + public static Element getModelElementWithId(Element rootElement, String attributeName, String modelId) { + Element modelElement = null; + List<Node> modelsList = getObjectsFromXPath(rootElement, MODEL_ROOT_ELEMENT + "/" + attributeName); + for (Node model : modelsList) { + if (model.getText().equals(modelId)) { + modelElement = model.getParent(); + } + } + return modelElement; + } + + /** + * Determines the validity of the supplied model element by checking if the name of the root element is correct. + * + * @param modelElement + * The model element to check. + * @return True if the root element matches the expected name. + */ + private static boolean isValidModel(Element modelElement) { + return MODEL_ROOT_ELEMENT.equals(modelElement.getName()); + } + + /** + * Strips all namespace information from the model XML. + * + * @param modelXML + * The model XML as a String. + * @return The model XML String minus the namespace information. + */ + private static String removeXmlStringNamespaceAndPreamble(String xmlString) { + return xmlString.replaceAll("(<\\?[^<]*\\?>)?", "") /* remove preamble */ + .replaceAll("xmlns.*?(\"|\').*?(\"|\')", "") /* remove xmlns declaration */ + .replaceAll("(<)(\\w+:)(.*?>)", "$1$3") /* remove opening tag prefix */ + .replaceAll("(</)(\\w+:)(.*?>)", "$1$3"); /* remove closing tags prefix */ + } +} diff --git a/src/main/java/org/onap/aai/validation/modeldriven/validator/InstanceReader.java b/src/main/java/org/onap/aai/validation/modeldriven/validator/InstanceReader.java new file mode 100644 index 0000000..7832e51 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/validator/InstanceReader.java @@ -0,0 +1,316 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.validator; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.jayway.jsonpath.DocumentContext; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import javax.inject.Inject; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.modeldriven.configuration.mapping.ModelInstanceMapper; +import org.onap.aai.validation.modeldriven.configuration.mapping.ModelInstanceMapper.MappingType; +import org.onap.aai.validation.reader.JsonReader; +import org.onap.aai.validation.reader.OxmReader; + +/** + * Reads values from an instance object. + */ +public class InstanceReader { + + private static final String MODEL_NAME = "model-name"; + private static final String[] INVALID_ENTRIES = { "inventory-response-items", "extra-properties", MODEL_NAME }; + private static final String RESOURCE_VERSION = "resource-version"; + private static final String JSON_PATH_MODEL_ID = "$.*.persona-model-id"; + + private JsonReader jsonReader; + private OxmReader oxmReader; + private JsonParser jsonParser = new JsonParser(); + + /** + * @param jsonReader + * @param oxmReader + */ + @Inject + public InstanceReader(JsonReader jsonReader, OxmReader oxmReader) { + this.jsonReader = jsonReader; + this.oxmReader = oxmReader; + } + + public OxmReader getOxmReader() { + return oxmReader; + } + + /** + * Gets object instance values. + * + * @param json + * a Named Query JSON payload + * @param mapping + * defines the paths that allow the extraction of values from the object instance. This includes: + * <ul> + * <li>origin: path that serves as the starting point for the instance search</li> + * <li>root: path to underlying instance objects that can be examined by recursively calling the + * getValues method</li> + * </ul> + * + * @return a {@link Multimap} of instances keyed by their model id. + * @throws ValidationServiceException + */ + public Multimap<String, String> getValues(String json, ModelInstanceMapper mapping) throws ValidationServiceException { + Multimap<String, String> values = HashMultimap.create(); + + DocumentContext document = jsonReader.parse(json); + + if (MappingType.RELATIONSHIP.equals(mapping.getMappingType())) { + String rootPath = mapping.getInstance().getRoot(); + if (rootPath == null || rootPath.isEmpty()) { + throw new ValidationServiceException(ValidationServiceError.INSTANCE_MAPPING_ROOT_ERROR); + } + + JsonElement jsonElement = jsonReader.getJsonElement(document, rootPath); + + if (jsonElement instanceof JsonArray) { + JsonArray jsonArray = jsonElement.getAsJsonArray(); + + processRelatedObjects(values, jsonArray); + } + } else { + // We are dealing with attributes. + String valuePath = mapping.getInstance().getValue(); + if (valuePath != null && !valuePath.isEmpty()) { + List<String> attributes = jsonReader.get(json, valuePath); + for (String attribute : attributes) { + values.put(attribute, null); + } + } + } + + return values; + } + + /** + * Gets the instance type, e.g. connector, pserver, etc. + * + * @param json + * a Named Query JSON payload + * @return the type of the entity + */ + public String getInstanceType(String json) { + return getNamedQueryEntity(json).getEntityType(); + } + + /** + * Gets the id of the instance. Uses the {@link OxmReader} to identify the property holding the primary key.<br> + * + * WARNING: Some types of object appear to have more than one primary key. This method uses the first primary key. + * + * @param json + * a Named Query JSON payload + * @return the identifier of the object instance + * @throws ValidationServiceException + */ + public String getInstanceId(String json) throws ValidationServiceException { + String instanceId = null; + + InstanceEntity entity = getNamedQueryEntity(json); + + List<String> primaryKeys = oxmReader.getPrimaryKeys(entity.getEntityType()); + + if (primaryKeys != null && !primaryKeys.isEmpty()) { + JsonObject instance = entity.getObject().getAsJsonObject(); + JsonElement primaryKey = instance.get(primaryKeys.get(0)); + instanceId = primaryKey == null ? null : primaryKey.getAsString(); + } + + return instanceId; + } + + /** + * Strips the instance out of its payload wrapping. + * + * @param json + * a Named Query JSON payload + * @param mappings + * the definition of the paths that allow the extraction of the instance from the JSON payload + * @return + * @throws ValidationServiceException + */ + public String getInstance(String json, List<ModelInstanceMapper> mappings) throws ValidationServiceException { + String origin = mappings.iterator().next().getInstance().getOrigin(); + List<String> jsonList = jsonReader.get(json, origin); + + if (!jsonList.isEmpty()) { + return jsonList.get(0); + } else { + throw new ValidationServiceException(ValidationServiceError.INSTANCE_READER_NO_INSTANCE, origin, json); + } + } + + /** + * Extracts the entity from a Named Query JSON payload. + * + * @param json + * a Named Query JSON payload + * @return an {@link InstanceEntity} object + */ + public InstanceEntity getNamedQueryEntity(String json) { + return getNamedQueryEntity(jsonParser.parse(json).getAsJsonObject()); + } + + /** + * Gets the model identifier of a given entity. + * + * @param entity + * a JSON entity + * @return a model identifier attribute value if the attribute exists else a null is returned. + * @throws ValidationServiceException + */ + public String getModelId(String entity) throws ValidationServiceException { + String modelId = null; + List<String> readResult = jsonReader.get(entity, JSON_PATH_MODEL_ID); + if (!readResult.isEmpty()) { + modelId = readResult.get(0); + } + return modelId; + } + + /** + * Gets the resource version of the instance. + * + * @param json + * a Named Query JSON payload + * @return the resource version of the object instance + */ + public String getResourceVersion(String json) { + String resourceVersion = null; + + InstanceEntity entity = getNamedQueryEntity(json); + + if (entity != null && entity.getObject() != null && entity.getObject().getAsJsonObject().has(RESOURCE_VERSION)) { + resourceVersion = entity.getObject().getAsJsonObject().get(RESOURCE_VERSION).getAsString(); + } + return resourceVersion; + } + + /** + * Gets the model name of the instance. + * + * @param jsonString + * a Named Query JSON payload + * @return the model name of the object instance + * @throws ValidationServiceException + */ + public String getModelName(String jsonString) { + JsonObject jsonObject = jsonParser.parse(jsonString).getAsJsonObject(); + return getModelName(jsonObject); + } + + /** + * @param jsonObject + * @return + */ + private String getModelName(JsonObject jsonObject) { + for (Entry<String, JsonElement> entry : jsonObject.entrySet()) { + if (MODEL_NAME.equals(entry.getKey())) { + return entry.getValue().getAsString(); + } + } + return null; + } + + private void processRelatedObjects(Multimap<String, String> values, JsonArray jsonArray) { + for (JsonElement relatedObject : jsonArray) { + JsonObject jsonObject = relatedObject.getAsJsonObject(); + + InstanceEntity entity = getNamedQueryEntity(jsonObject); + if (entity != null) { + values.put(entity.getModelName() == null ? entity.getEntityType() : entity.getModelName(), jsonObject.toString()); + } + } + } + + private InstanceEntity getNamedQueryEntity(JsonObject jsonObject) { + Set<Entry<String, JsonElement>> entrySet = jsonObject.entrySet(); + + String modelName = getModelName(jsonObject); + + for (Entry<String, JsonElement> entry : entrySet) { + if (!Arrays.asList(INVALID_ENTRIES).contains(entry.getKey())) { + return new InstanceEntity(entry.getKey(), modelName, entry.getValue().getAsJsonObject(), jsonObject); + } + } + + return null; + } + + /** + * An Entity bean for the InstanceReader + * + */ + public class InstanceEntity { + + private String entityType; + private String modelName; + private JsonObject object; + private JsonObject objectAndGraph; + + /** + * @param entityType + * @param modelName + * @param object + * @param objectAndGraph + */ + public InstanceEntity(String entityType, String modelName, JsonObject object, JsonObject objectAndGraph) { + this.entityType = entityType; + this.modelName = modelName; + this.object = object; + this.objectAndGraph = objectAndGraph; + } + + public String getEntityType() { + return entityType; + } + + public String getModelName() { + return modelName; + } + + public JsonObject getObject() { + return object; + } + + public JsonObject getObjectAndGraph() { + return objectAndGraph; + } + + @Override + public String toString() { + return "Entity [entityType=" + entityType + ", modelName=" + modelName + ", object=" + object.toString() + ", fullObject=" + + objectAndGraph.toString() + "]"; + } + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/modeldriven/validator/ModelDrivenValidator.java b/src/main/java/org/onap/aai/validation/modeldriven/validator/ModelDrivenValidator.java new file mode 100644 index 0000000..1b8ab00 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/validator/ModelDrivenValidator.java @@ -0,0 +1,306 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.validator; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import javax.inject.Inject; +import org.apache.commons.collections.CollectionUtils; +import org.dom4j.Node; +import org.onap.aai.validation.Validator; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.modeldriven.ModelCacheManager; +import org.onap.aai.validation.modeldriven.ModelId; +import org.onap.aai.validation.modeldriven.configuration.mapping.ModelInstanceMapper; +import org.onap.aai.validation.modeldriven.configuration.mapping.ModelInstanceMappingReader; +import org.onap.aai.validation.modeldriven.configuration.mapping.ModelInstanceMapper.MappingType; +import org.onap.aai.validation.reader.EntityReader; +import org.onap.aai.validation.reader.EventReader; +import org.onap.aai.validation.reader.InstanceEntityReader; +import org.onap.aai.validation.reader.data.Entity; +import org.onap.aai.validation.reader.data.EntityId; +import org.onap.aai.validation.result.ValidationResult; +import org.onap.aai.validation.result.Violation; +import org.onap.aai.validation.result.Violation.Builder; +import org.onap.aai.validation.result.Violation.ViolationType; + +/** + * Validates object instances against the ONAP model. + * + */ +public class ModelDrivenValidator implements Validator { + + /** + * Types of validation rules. + */ + public enum RuleType { + REL, ATTR + } + + /** + * Types of validation outcomes. + */ + public enum ValidationOutcomeType { + NO_MODEL, MISSING, UNEXPECTED; + } + + private ModelCacheManager modelCacheManager; + private List<ModelInstanceMapper> mappings; + private InstanceReader instanceReader; + private EventReader eventReader; + + /** + * Constructor defining injected dependencies. + * + * @param modelCacheManager + * a cache manager for the models + * @param modelInstanceMappingReader + * a configuration reader to provide model and instance mapping + * @param instanceReader + * a reader of A&AI instances + * @param eventReader + * @throws ValidationServiceException + */ + @Inject + public ModelDrivenValidator(ModelCacheManager modelCacheManager, ModelInstanceMappingReader modelInstanceMappingReader, InstanceReader instanceReader, + EventReader eventReader) throws ValidationServiceException { + this.modelCacheManager = modelCacheManager; + this.mappings = modelInstanceMappingReader.getMappings(); + this.instanceReader = instanceReader; + this.eventReader = eventReader; + } + + @Override + public void initialise() throws ValidationServiceException { + // Deliberately empty + } + + /** + * Validates the given event instance against the ECOMP model. + * + * @param eventInstance + * the event instance to be validated + * @return {@link Violation} with the results of the object validation + * @throws ValidationServiceException + */ + @Override + public List<ValidationResult> validate(String eventInstance) throws ValidationServiceException { + // Read event json into Entity bean + Entity eventEntity = eventReader.getEntity(eventInstance); + + EntityReader reader = new InstanceEntityReader(instanceReader); + String entityJson = instanceReader.getInstance(eventEntity.getJson(), mappings); + Entity instanceEntity = new Entity(entityJson, instanceReader.getInstanceType(entityJson), eventEntity.getEntityLink(), reader); + + // Get model ID from object instance and retrieve corresponding model. + ModelId modelId = new ModelId(ModelId.ATTR_MODEL_ID, instanceReader.getModelId(instanceEntity.getJson())); + Node modelElement = modelCacheManager.get(modelId); + + List<Violation> violations = new ArrayList<>(); + + if (modelElement == null) { + ViolationInfo info = ViolationInfo.valueOf(ModelDrivenValidator.ValidationOutcomeType.NO_MODEL.toString()); + Map<String, Object> details = new HashMap<>(); + details.put("No model ID", modelId.getModelId()); + Violation.Builder builder = new Violation.Builder(instanceEntity).violationType(ViolationType.MODEL); + builder.category(info.getCategory()).severity(info.getSeverity()).violationDetails(details) + .errorMessage(info.getErrorMessage(modelId.getModelId())); + violations.add(builder.build()); + } else { + // Validate model with instance according to mappings. + for (ModelInstanceMapper mapping : mappings) { + List<Violation> currentViolations = new ArrayList<>(); + + // If we are validating related objects, find the first valid child object to begin validating from. + Node validModelElement = modelElement; + if (MappingType.RELATIONSHIP.equals(mapping.getMappingType()) && !ModelReader.isValidModelType(modelElement, mapping)) { + Multimap<String, Node> models = HashMultimap.create(); + ModelReader.getValuesAndModels(modelElement, mapping, modelCacheManager, models); + validModelElement = models.isEmpty() ? modelElement : models.values().iterator().next(); + } + + validateAllRecursive(validModelElement, instanceEntity, mapping, currentViolations, reader); + violations.addAll(currentViolations); + } + } + + ValidationResult validationResult = new ValidationResult(instanceEntity); + + // This is a shortcut to passing the parent model name all the way down. + populateViolationModelNames(violations, instanceEntity); + + validationResult.addViolations(violations); + + return Arrays.asList(validationResult); + } + + /* + * Recursively validates all child entities starting with the crown widget. + */ + private void validateAllRecursive(Node currentModelNode, Entity entity, ModelInstanceMapper mapping, List<Violation> validations, EntityReader reader) + throws ValidationServiceException { + String entityLink = null; + Multimap<String, Node> modelMap = ModelReader.getValues(currentModelNode, mapping, modelCacheManager); + Multimap<String, String> instanceMap = instanceReader.getValues(entity.getJson(), mapping); + + // Validate model with instance according to mappings. + // Note: Currently the cardinality of instances are not validated. + List<Violation> currentViolations = validateModelInstanceValues(modelMap.keySet(), instanceMap.keySet(), entity, mapping); + validations.addAll(currentViolations); + + // Remove erroring objects from the maps so that we don't validate their children. + for (Violation currentViolation : currentViolations) { + String entityType = (String) currentViolation.getViolationDetails().get(Violation.ENTITY_TYPE_PROPERTY); + if (entityType != null) { + modelMap.removeAll(entityType); + instanceMap.removeAll(entityType); + } + } + + // Continue down the model hierarchy for objects that did not error in the current layer. + for (Entry<String, Node> modelEntry : modelMap.entries()) { + // Get the child model. + Node childModelNode = modelEntry.getValue(); + if (childModelNode != null) { + // Validate all child instance objects with current child model. + Collection<String> childInstanceObjects = instanceMap.get(modelEntry.getKey()); + for (String childInstanceObject : childInstanceObjects) { + Entity childEntity = new Entity(childInstanceObject, instanceReader.getInstanceType(childInstanceObject), entityLink, reader); + validateAllRecursive(childModelNode, childEntity, mapping, validations, reader); + } + } + } + } + + /** + * Compares the List of values found in the model with the values found in the Json and generates validation errors + * based on the differences. + * + * @param modelValues + * the values found in the model + * @param instanceValues + * the values found in the Json. + * @param entity + * @param modelInstanceMapper + * the mappings used to to find the model and instance values + * @return List of Validation objects representing the errors found during validation. + * @throws ValidationServiceException + */ + private List<Violation> validateModelInstanceValues(Set<String> modelValues, Set<String> instanceValues, Entity entity, + ModelInstanceMapper modelInstanceMapper) throws ValidationServiceException { + List<Violation> violations = new ArrayList<>(); + + Collection<?> missingValues = CollectionUtils.subtract(modelValues, instanceValues); + violations.addAll(setViolationDetails(ValidationOutcomeType.MISSING, missingValues, entity, modelInstanceMapper)); + + Collection<?> unexpectedValues = CollectionUtils.subtract(instanceValues, modelValues); + violations.addAll(setViolationDetails(ValidationOutcomeType.UNEXPECTED, unexpectedValues, entity, modelInstanceMapper)); + + return violations; + } + + /* + * Sets the list of {@link Violation} objects with all the violation details. + * + * @param outcome + * + * @param values the values that are causing the violation + * + * @param entity the entity being validated + * + * @param modelInstanceMapper the mappings used to to find the model and instance values + * + * @return list of {@link Violation} objects set by this method + * + * @throws ValidationServiceException + */ + private List<Violation> setViolationDetails(ValidationOutcomeType outcome, Collection<?> values, Entity entity, ModelInstanceMapper modelInstanceMapper) + throws ValidationServiceException { + + List<Violation> violations = new ArrayList<>(); + Builder builder = new Builder(entity).violationType("Model"); + + for (Object value : values) { + RuleType ruleType = modelInstanceMapper.getMappingType().equals(MappingType.RELATIONSHIP) ? RuleType.REL : RuleType.ATTR; + String category = outcome.toString() + "_" + ruleType; + + ViolationInfo info = ViolationInfo.valueOf(category); + builder.category(info.getCategory()); + builder.severity(info.getSeverity()); + + Map<String, Object> details = new HashMap<>(); + details.put(outcome.toString() + " " + ruleType.toString(), value); + + switch (ruleType) { + case ATTR: + builder.errorMessage(info.getErrorMessage(value)); + break; + case REL: + buildEntityIdDetails(details, entity); + builder.errorMessage(info.getErrorMessage(entity.getIds().toString(), entity.getType(), value)); + break; + default: + // Do nothing + break; + } + builder.violationDetails(details); + violations.add(builder.build()); + } + + return violations; + } + + /* + * Add entity IDs to the violation details + * + * @param details the violation details to populate + * + * @param entity + * + * @throws ValidationServiceException + */ + private void buildEntityIdDetails(Map<String, Object> details, Entity entity) throws ValidationServiceException { + JsonObject entityIdsObject = new JsonObject(); + for (EntityId entityId : entity.getIds()) { + entityIdsObject.addProperty(entityId.getPrimaryKey(), entityId.getValue()); + } + details.put(Violation.ENTITY_ID_PROPERTY, entityIdsObject); + details.put(Violation.ENTITY_TYPE_PROPERTY, entity.getType()); + details.put(Violation.ENTITY_MODELNAME_PROPERTY, instanceReader.getModelName(entity.getJson())); + } + + /* + * Sets the model name attribute on all violations in the list. The model name is retrieved from the entity + * provided. + */ + private void populateViolationModelNames(List<Violation> violations, Entity entity) { + String modelName = instanceReader.getModelName(entity.getJson()); + for (Violation violation : violations) { + violation.setModelName(modelName); + } + } +} diff --git a/src/main/java/org/onap/aai/validation/modeldriven/validator/ModelReader.java b/src/main/java/org/onap/aai/validation/modeldriven/validator/ModelReader.java new file mode 100644 index 0000000..04728a3 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/validator/ModelReader.java @@ -0,0 +1,236 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.validator; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.dom4j.Node; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.modeldriven.ModelCacheManager; +import org.onap.aai.validation.modeldriven.ModelId; +import org.onap.aai.validation.modeldriven.configuration.mapping.Filter; +import org.onap.aai.validation.modeldriven.configuration.mapping.ModelInstanceMapper; +import org.onap.aai.validation.modeldriven.configuration.mapping.ModelInstanceMapper.MappingType; +import org.onap.aai.validation.modeldriven.parser.XMLModelParser; + +/** + * Reads values from the model. + */ +public class ModelReader { + + private static final String ATTRIBUTE_MODELTYPE = "model-type"; + + /** + * Do not instantiate an object of this class + */ + private ModelReader() { + // Deliberately empty + } + + /** + * Gets the values of a model element as defined by the model-instance mapping configuration. When the mapping + * type is "attribute", the multimap will be returned with a null value. + * + * @param modelElement + * the model element from which the values will be extracted + * @param mapping + * the model-instance mapping object defining the path to the model values + * @param modelCacheManager + * the model cache manager used to retrieve further models + * @return a {@link Multimap} of model values. + * @throws ValidationServiceException + */ + public static Multimap<String, Node> getValues(Node modelElement, ModelInstanceMapper mapping, ModelCacheManager modelCacheManager) + throws ValidationServiceException { + Multimap<String, Node> values = HashMultimap.create(); + + if (MappingType.ATTRIBUTE.equals(mapping.getMappingType())) { + // Get attributes on current model element. + Multimap<String, Node> modelValues = getModelValues(modelElement, mapping, false); + if (modelValues.isEmpty()) { + throw new ValidationServiceException(ValidationServiceError.MODEL_VALUE_ERROR, + mapping.getModel().getValue(), modelElement.asXML()); + } + values.putAll(modelValues); + } else { + // Get related objects. + getValuesAndModels(modelElement, mapping, modelCacheManager, values); + } + + + return values; + } + + /** + * Returns the model type property of the current model element. + * + * @param model + * The current model element. + * @return the model type of the current element or null if not found. + */ + public static String getModelType(Node model) { + String modelType = null; + List<Node> modelTypeElements = XMLModelParser.getObjectsFromXPath(model, ATTRIBUTE_MODELTYPE); + if (!modelTypeElements.isEmpty()) { + modelType = modelTypeElements.iterator().next().getText(); + } + return modelType; + } + + /** + * @param model + * @param mapping + * @return True if supplied model is of type widget. + */ + public static boolean isValidModelType(Node model, ModelInstanceMapper mapping) { + Collection<String> validTypes = mapping.getModel().getFilter().getValid(); + return validTypes.isEmpty() ? false :validTypes.contains(getModelType(model)); + } + + /** + * Populates a Multimap of models. If a root and filter are defined in the mapping it will navigate the model to find a + * valid models according to the filter. If the root property is not defined a model is not returned. + * + * @param model + * the model to be inspected + * @param mapping + * the model-instance mapping object defining the root model + * @param modelCacheManager + * the model cache manager used to retrieve further models + * @param models + * a Multimap of models that will be populated with further models + * @throws ValidationServiceException + */ + public static void getValuesAndModels(Node model, ModelInstanceMapper mapping, ModelCacheManager modelCacheManager, Multimap<String, Node> models) throws ValidationServiceException { + String root = mapping.getModel().getRoot(); + + if (root == null) { + return; + } + + List<Node> childModelElements = XMLModelParser.getObjectsFromXPath(model, root); + for (Node childModel : childModelElements) { + // If the child element is a leaf, this could either mean the end of the hierarchy, or that we have + // encountered a resource and need to retrieve a separate model to continue the model traversal. + List<String> modelNames = getModelValuesList(childModel, mapping); + if (!hasChildren(childModel, root) && !isValidModel(childModel, mapping) && mapping.getModel().getId() != null) { + childModel = getChildModelNode(modelCacheManager, childModel, mapping); + } + + if (isValidModel(childModel, mapping)) { + for (String modelName : modelNames) { + models.put(modelName, childModel); + } + } else { + getValuesAndModels(childModel, mapping, modelCacheManager, models); + } + } + } + + /** + * Find the next child model given a specific node. + * + * @param modelCacheManager + * the model cache manager used to retrieve further models + * @param node + * the top-level node under which child model nodes are searched + * @param rootXPath + * the path expression to apply to the node to find child elements + * @param modelIdPath + * the path expression to apply to the node to find the child model IDs + * @return either or the {@code node} if there were no matches for {@code id} + * @throws ValidationServiceException + */ + private static Node getChildModelNode(ModelCacheManager modelCacheManager, Node node, ModelInstanceMapper mapping) throws ValidationServiceException { + Node childModel = node; + + // Get the model for the specified node to check its type. + // Only one model ID is expected, although the API returns a list. + List<Node> childModelIds = XMLModelParser.getObjectsFromXPath(node, mapping.getModel().getId()); + + if (!childModelIds.isEmpty()) { + // Found the child model ID, so retrieve the child model from cache. + ModelId modelId = new ModelId(ModelId.ATTR_MODEL_NAME_VERSION_ID, childModelIds.iterator().next().getText()); + Node fullChildModel = modelCacheManager.get(modelId); + + if (fullChildModel != null && !isValidModelType(fullChildModel, mapping)) { + // Child model is not a widget so replace current child model with the full child model + // retrieved from the cache. + List<Node> fullChildModelElements = XMLModelParser.getObjectsFromXPath(fullChildModel, mapping.getModel().getRoot()); + // Only one crown widget is expected, although the API returns a list. + childModel = fullChildModelElements.isEmpty() ? node : fullChildModelElements.iterator().next(); + } + } + + return childModel; + } + + private static Multimap<String, Node> getModelValues(Node model, ModelInstanceMapper mapping, boolean addModel) { + Multimap<String, Node> values = HashMultimap.create(); + List<String> valueStrings = getModelValuesList(model, mapping); + for (String value : valueStrings) { + values.put(value, addModel ? model : null); + } + return values; + } + + private static List<String> getModelValuesList(Node model, ModelInstanceMapper mapping) { + List<String> values = new ArrayList<>(); + List<Node> valueElements = XMLModelParser.getObjectsFromXPath(model, mapping.getModel().getValue()); + for (Node node : valueElements) { + values.add(node.getText()); + } + return values; + } + + private static boolean isValidModel(Node node, ModelInstanceMapper mapping) { + Filter filter = mapping.getModel().getFilter(); + if (filter == null) { + return true; + } + + List<String> valid = filter.getValid(); + // If there are no valid values, return false. + if (valid.isEmpty()) { + return false; + } + + String filterXPath = filter.getPath(); + if (filterXPath == null) { + return false; + } + + List<Node> filterNodes = XMLModelParser.getObjectsFromXPath(node, filterXPath); + for (Node filterNode : filterNodes) { + String text = filterNode.getText(); + if (valid.contains(text)) { + return true; + } + } + + return false; + } + + private static boolean hasChildren(Node parent, String rootXPath) { + return !XMLModelParser.getObjectsFromXPath(parent, rootXPath).isEmpty(); + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/modeldriven/validator/ViolationInfo.java b/src/main/java/org/onap/aai/validation/modeldriven/validator/ViolationInfo.java new file mode 100644 index 0000000..e452d73 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/modeldriven/validator/ViolationInfo.java @@ -0,0 +1,88 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.modeldriven.validator; + +import java.text.MessageFormat; +import org.onap.aai.validation.controller.ValidationController; + +/** + * Defines and formats the violation information. + */ +public enum ViolationInfo { + //@formatter:off + NO_MODEL ("NO_MODEL", ValidationController.VALIDATION_ERROR_SEVERITY, "No model ID=[{0}]", "The model [{0}] could not be found"), + MISSING_ATTR ("MISSING_ATTR", ValidationController.VALIDATION_ERROR_SEVERITY, "{0} {1}=[{2}]", "Attribute [{0}] is missing in the object instance"), + UNEXPECTED_ATTR ("UNEXPECTED_ATTR", ValidationController.VALIDATION_ERROR_SEVERITY, "{0} {1}=[{2}]", "Attribute [{0}] should not be present in the object instance"), + MISSING_REL ("MISSING_REL", ValidationController.VALIDATION_ERROR_SEVERITY, "entityId=[{0}] entityType=[{1}], {2} {3}=[{4}]", "Entity {0} of type [{1}] must be related to [{2}]"), + UNEXPECTED_REL ("UNEXPECTED_REL", ValidationController.VALIDATION_ERROR_SEVERITY, "entityId=[{0}] entityType=[{1}], {2} {3}=[{4}]", "Entity {0} of type [{1}] must not be related to [{2}]"); + //@formatter:on + + private String category; + private String severity; + private String violationDetails; + private String errorMessage; + + /** + * @param category + * @param severity + * @param violationDetails + * @param errorMessage + */ + private ViolationInfo(String category, String severity, String violationDetails, String errorMessage) { + this.category = category; + this.severity = severity; + this.violationDetails = violationDetails; + this.errorMessage = errorMessage; + } + + /** + * @return + */ + public String getCategory() { + return this.category; + } + + /** + * @return + */ + public String getSeverity() { + return this.severity; + } + + /** + * @param args + * @return + */ + public String getViolationDetails(Object... args) { + return formatter(this.violationDetails, args); + } + + /** + * @param args + * @return + */ + public String getErrorMessage(Object... args) { + return formatter(this.errorMessage, args); + } + + private String formatter(String violationInfo, Object... args) { + MessageFormat formatter = new MessageFormat(""); + formatter.applyPattern(violationInfo); + return formatter.format(args); + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/publisher/MessagePublisher.java b/src/main/java/org/onap/aai/validation/publisher/MessagePublisher.java new file mode 100644 index 0000000..94e1f29 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/publisher/MessagePublisher.java @@ -0,0 +1,47 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.publisher; + +import java.util.Collection; +import org.onap.aai.validation.exception.ValidationServiceException; + +/** + * A Publisher of messages. + * + */ +public interface MessagePublisher { + + /** + * Sends a message somewhere. + * + * @param message + * The String message to send. + * @throws ValidationServiceException + */ + void publishMessage(String message) throws ValidationServiceException; + + /** + * Sends a Collection of messages somewhere. + * + * @param messages + * The String messages to send. + * @throws ValidationServiceException + */ + void publishMessages(Collection<String> messages) throws ValidationServiceException; + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/publisher/ValidationEventPublisher.java b/src/main/java/org/onap/aai/validation/publisher/ValidationEventPublisher.java new file mode 100644 index 0000000..0cebbf9 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/publisher/ValidationEventPublisher.java @@ -0,0 +1,164 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.publisher; + +import org.onap.aai.event.client.DMaaPEventPublisher; +import org.onap.aai.validation.config.TopicAdminConfig; +import org.onap.aai.validation.config.TopicConfig; +import org.onap.aai.validation.config.TopicConfig.Topic; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.factory.DMaaPEventPublisherFactory; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.inject.Inject; + +/** + * Event Publisher + * + */ +public class ValidationEventPublisher implements MessagePublisher { + + private static LogHelper applicationLogger = LogHelper.INSTANCE; + + private List<Topic> publisherTopics; + + private boolean enablePublishing; + + private long retries; + + private long retriesRemaining; + + private DMaaPEventPublisherFactory dMaapFactory; + + + /** + * Instantiates an Event Publisher instance using properties from config file. + * + * @param topicConfig + * @param topicAdminConfig + */ + @Inject + public ValidationEventPublisher(TopicConfig topicConfig, TopicAdminConfig topicAdminConfig) { + enablePublishing = topicAdminConfig.isPublishEnable(); + if (enablePublishing) { + publisherTopics = topicConfig.getPublisherTopics(); + retries = topicAdminConfig.getPublishRetries(); + } + dMaapFactory = new DMaaPEventPublisherFactory(); + } + + /** + * Connect to the event publisher, add the message, and then publish it by closing the publisher. + */ + @Override + public void publishMessage(String message) throws ValidationServiceException { + Collection<String> messages = new ArrayList<>(); + messages.add(message); + publishMessages(messages); + } + + /** + * Connect to the event publisher, adds the messages, and then publish them by closing the publisher. + */ + @Override + public void publishMessages(Collection<String> messages) throws ValidationServiceException { + if (!enablePublishing) { + return; + } else { + applicationLogger.debug("Publishing messages: " + messages); + for (Topic topic : publisherTopics) { + retriesRemaining = retries; + publishMessages(messages, topic); + } + } + } + + private void publishMessages(Collection<String> messages, Topic topic) throws ValidationServiceException { + + DMaaPEventPublisher dMaapEventPublisher = dMaapFactory.createEventPublisher(topic.getHost(), topic.getName(), topic.getUsername(), + topic.getPassword(), topic.getTransportType()); + + try { + // Add our message to the publisher's queue/bus + int result = dMaapEventPublisher.sendSync(topic.getPartition(), messages); + if (result != messages.size()) { + applicationLogger.warn(ApplicationMsgs.UNSENT_MESSAGE_WARN); + closeEventPublisher(dMaapEventPublisher); + retryOrThrow(messages, topic, new ValidationServiceException( + ValidationServiceError.EVENT_CLIENT_INCORRECT_NUMBER_OF_MESSAGES_SENT, result)); + } + } catch (Exception e) { + applicationLogger.error(ApplicationMsgs.UNSENT_MESSAGE_ERROR); + closeEventPublisher(dMaapEventPublisher); + retryOrThrow(messages, topic, + new ValidationServiceException(ValidationServiceError.EVENT_CLIENT_SEND_ERROR, e)); + } + + completeMessageSending(dMaapEventPublisher, topic); + } + + /** + * Publish the queued messages by closing the publisher. + * + * @param eventPublisher the publisher to close + * @throws AuditException + */ + private void completeMessageSending(DMaaPEventPublisher eventPublisher, Topic topic) + throws ValidationServiceException { + List<String> unsentMsgs = closeEventPublisher(eventPublisher); + + if (unsentMsgs != null && !unsentMsgs.isEmpty()) { + // Log the error, as the exception will not be propagated due to the fact that the Cambria Client throws + // an exception first in a separate thread. + applicationLogger.error(ApplicationMsgs.EVENT_CLIENT_CLOSE_UNSENT_MESSAGE, + ValidationServiceError.EVENT_CLIENT_CLOSE_UNSENT_MESSAGE.getMessage(unsentMsgs)); + + retryOrThrow(unsentMsgs, topic, new ValidationServiceException( + ValidationServiceError.EVENT_CLIENT_CLOSE_UNSENT_MESSAGE, unsentMsgs)); + } + } + + private void retryOrThrow(Collection<String> messages, Topic topic, ValidationServiceException exceptionToThrow) + throws ValidationServiceException { + if (retriesRemaining <= 0) { + applicationLogger.warn(ApplicationMsgs.SEND_MESSAGE_ABORT_WARN); + throw exceptionToThrow; + } else { + applicationLogger.warn(ApplicationMsgs.SEND_MESSAGE_RETRY_WARN); + retriesRemaining--; + publishMessages(messages, topic); + } + } + + private List<String> closeEventPublisher(DMaaPEventPublisher eventPublisher) throws ValidationServiceException { + try { + return eventPublisher.closeWithUnsent(); + } catch (Exception e) { + throw new ValidationServiceException(ValidationServiceError.EVENT_CLIENT_CLOSE_ERROR, e); + } + } + + public void setEventPublisherFactory(DMaaPEventPublisherFactory dMaapFactory) { + this.dMaapFactory = dMaapFactory; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/reader/EntityReader.java b/src/main/java/org/onap/aai/validation/reader/EntityReader.java new file mode 100644 index 0000000..f55af2f --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/EntityReader.java @@ -0,0 +1,61 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader; + +import java.util.List; +import java.util.Optional; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.reader.data.EntityId; + +/** + * Interface for extracting values from an entity (in JSON format). + * + */ +public interface EntityReader { + + /** + * Return the value found at the supplied path. + * + * @param json + * the JSON representation of the entity + * @param path + * specifier of the path to the value within the JSON entity + * @return either a primitive object (e.g. String, Integer) or a JSON element + * @throws ValidationServiceException + */ + Object getObject(String json, String path) throws ValidationServiceException; + + /** + * @param json + * the JSON representation of the entity + * @param type + * the type of the entity + * @return the key value(s) identifying the entity + * @throws ValidationServiceException + */ + List<EntityId> getIds(String json, String type) throws ValidationServiceException; + + /** + * @param json + * the JSON representation of the entity + * @return the resource version of the entity (if present) + * @throws ValidationServiceException + */ + Optional<String> getResourceVersion(String json) throws ValidationServiceException; + +} diff --git a/src/main/java/org/onap/aai/validation/reader/EventEntityReader.java b/src/main/java/org/onap/aai/validation/reader/EventEntityReader.java new file mode 100644 index 0000000..7f285f8 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/EventEntityReader.java @@ -0,0 +1,118 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.onap.aai.validation.config.EventReaderConfig; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.reader.data.EntityId; + +/** + * An entity reader for reading A&AI events. This implementation is used by the EventReader + * + */ +public class EventEntityReader implements EntityReader { + + private JsonReader jsonReader; + private OxmReader oxmReader; + + private EventReaderConfig config; + + /** + * @param eventReaderConfig + * @param jsonReader + * @param oxmReader + */ + public EventEntityReader(final EventReaderConfig eventReaderConfig, final JsonReader jsonReader, + final OxmReader oxmReader) { + this.jsonReader = jsonReader; + this.oxmReader = oxmReader; + this.config = eventReaderConfig; + } + + /** + * Parse the supplied json and return the content (values) specified by the supplied path + * + * @param json + * @param path + * @return either a String or an Array of objects + * @throws ValidationServiceException + */ + @Override + public Object getObject(String json, String path) throws ValidationServiceException { + return jsonReader.getObject(jsonReader.parse(json), path); + } + + public String getEntityResourceVersionPath() { + return config.getEntityResourceVersionPath(); + } + + @Override + public List<EntityId> getIds(String json, String type) throws ValidationServiceException { + List<EntityId> ids = new ArrayList<>(); + for (String pk : oxmReader.getPrimaryKeys(type)) { + String pkPaths = config.getEntityIdPath(pk); + String pkValue = getPropertyForMultiplePaths(json, pkPaths).orElseThrow( + () -> new ValidationServiceException(ValidationServiceError.EVENT_READER_MISSING_PROPERTY, + pkPaths)); + ids.add(new EntityId(pk, pkValue)); + } + return ids; + } + + /** + * Get an entity property from an entity in JSON format. + * + * @param json the entity + * @param path the JSON path to the property + * @return an optional property value + * @throws ValidationServiceException + */ + public Optional<String> getProperty(String json, String path) throws ValidationServiceException { + return jsonReader.get(json, path).stream().findFirst(); + } + + @Override + public Optional<String> getResourceVersion(String json) throws ValidationServiceException { + return getPropertyForMultiplePaths(json, getEntityResourceVersionPath()); + } + + /** + * Takes a comma separated list of jsonpaths and applies each one in turn until a value is found. + * + * @param json The json to search + * @param multiplePaths Comma separated list of jsonpath strings + * @return The value of the first jsonpath string that returns a value + * @throws ValidationServiceException + */ + private Optional<String> getPropertyForMultiplePaths(String json, String multiplePaths) + throws ValidationServiceException { + Optional<String> propertyValue = Optional.empty(); + for (String path : multiplePaths.split(",")) { + propertyValue = getProperty(json, path); + if (propertyValue.isPresent()) { + break; + } + } + return propertyValue; + } + +} diff --git a/src/main/java/org/onap/aai/validation/reader/EventReader.java b/src/main/java/org/onap/aai/validation/reader/EventReader.java new file mode 100644 index 0000000..614ccbf --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/EventReader.java @@ -0,0 +1,215 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader; + +import com.jayway.jsonpath.DocumentContext; +import java.util.List; +import java.util.Optional; +import org.onap.aai.validation.config.EventReaderConfig; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.reader.data.Entity; +import org.onap.aai.validation.util.StringUtils; + +/** + * Reads event objects. + * + */ +public class EventReader { + + private EventReaderConfig eventReaderConfig; + + private JsonReader jsonReader; + + private EntityReader entityReader; + + /** + * + * @param eventReaderConfig the event reader configuration including paths to event properties + * @param jsonReader a JSON reader + * @param oxmReader an OXM reader to retrieve the primary key names for the entity + */ + public EventReader(final EventReaderConfig eventReaderConfig, final JsonReader jsonReader, + final OxmReader oxmReader) { + this.eventReaderConfig = eventReaderConfig; + this.jsonReader = jsonReader; + this.entityReader = new EventEntityReader(eventReaderConfig, jsonReader, oxmReader); + } + + /////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + /////////////////////////////////////////////////////////////////////////// + + /** + * Get the domain of the event. + * + * @param event a JSON String with the event contents + * @return the domain of the event + * @throws ValidationServiceException + */ + public Optional<String> getEventDomain(String event) throws ValidationServiceException { + List<String> readerResult = jsonReader.get(event, eventReaderConfig.getEventDomainPath()); + return getFirst(readerResult); + } + + /** + * Get the action of the event. + * + * @param event a JSON String with the event contents + * @return the action of the event + * @throws ValidationServiceException + */ + public Optional<String> getEventAction(String event) throws ValidationServiceException { + List<String> readerResult = jsonReader.get(event, eventReaderConfig.getEventActionPath()); + return getFirst(readerResult); + } + + /** + * Get the type of the event. + * + * @param event a JSON String with the event contents + * @return the type of the event + * @throws ValidationServiceException + */ + public Optional<String> getEventType(String event) throws ValidationServiceException { + List<String> readerResult = jsonReader.get(event, eventReaderConfig.getEventTypePath()); + return getFirst(readerResult); + } + + /** + * Get the entity type of the entity in the event. + * + * @param event a JSON String with the event contents + * @return the type of the entity + * @throws ValidationServiceException + */ + public Optional<String> getEntityType(String event) throws ValidationServiceException { + List<String> readerResult = jsonReader.get(event, eventReaderConfig.getEntityTypePath()); + return getFirst(readerResult); + } + + /** + * Get the entity contained in the event. + * + * @param event a JSON String with the event contents + * @return the entity + */ + public Entity getEntity(String event) throws ValidationServiceException { + DocumentContext document = jsonReader.parse(event); + + String entityType = getValue(document, eventReaderConfig.getEntityTypePath()) + .orElseThrow(() -> new ValidationServiceException(ValidationServiceError.EVENT_READER_MISSING_PROPERTY, + eventReaderConfig.getEntityTypePath())); + String topEntityType = getValue(document, eventReaderConfig.getTopEntityTypePath()).orElse(entityType); + String entityLink = getEntityLink(document); + String json = findEntity(event, topEntityType, entityType); + + return new Entity(json, entityType, entityLink, entityReader); + } + + /** + * Get the value of the JSON property defined by the path. + * + * @param json a JSON string + * @param path the path to a property + * @return the value + * @throws ValidationServiceException if the value is not present + */ + public String getValue(final String json, final String path) throws ValidationServiceException { + return getFirst(jsonReader.get(json, path)).orElseThrow( + () -> new ValidationServiceException(ValidationServiceError.EVENT_READER_MISSING_PROPERTY, path)); + } + + /////////////////////////////////////////////////////////////////////////// + // PRIVATE METHODS + /////////////////////////////////////////////////////////////////////////// + + private String findEntity(String event, String topEntityType, String entityType) throws ValidationServiceException { + String json; + if (entityType.equals(topEntityType)) { + json = getValue(event, eventReaderConfig.getEntityPath()); + } else { + json = findNestedEntity(event, eventReaderConfig.getNestedEntityPath(entityType)); + } + return json; + } + + /** + * @param event + * @param path + * @return + * @throws ValidationServiceException + */ + private String findNestedEntity(String event, String path) throws ValidationServiceException { + List<String> entities = jsonReader.get(event, path); + if (entities.isEmpty()) { + throw new ValidationServiceException(ValidationServiceError.EVENT_READER_MISSING_PROPERTY, path); + } else if (entities.size() > 1) { + throw new ValidationServiceException(ValidationServiceError.EVENT_READER_TOO_MANY_ENTITIES); + } + return entities.get(0); + } + + private Optional<String> getFirst(List<String> l) { + return l.stream().findFirst(); + } + + private Optional<String> getValue(final DocumentContext document, final String path) { + return getFirst(jsonReader.getAsList(document, path)); + } + + /** + * Gets the entity link from the event but altered by stripping a prefix identified by a delimiter configured in the + * event reader properties configuration. + * + * @param document the parsed JSON event + * @return the entity link + * @throws ValidationServiceException + */ + private String getEntityLink(DocumentContext document) throws ValidationServiceException { + String entityLink = getValue(document, eventReaderConfig.getEntityLinkPath()).orElse(""); + String strippedEntityLink = null; + try { + strippedEntityLink = StringUtils.stripPrefixRegex(entityLink, eventReaderConfig.getEntityLinkDelimiter()); + } catch (ValidationServiceException e) { + throw new ValidationServiceException(ValidationServiceError.EVENT_READER_PROPERTY_READ_ERROR, e); + } + return strippedEntityLink; + } + + /////////////////////////////////////////////////////////////////////////// + // GETTERS AND SETTERS + /////////////////////////////////////////////////////////////////////////// + + /** + * + * @return event configuration + */ + public EventReaderConfig getEventReaderConfig() { + return eventReaderConfig; + } + + /** + * + * @param eventReaderConfig event configuration + */ + public void setEventReaderConfig(EventReaderConfig eventReaderConfig) { + this.eventReaderConfig = eventReaderConfig; + } + +} diff --git a/src/main/java/org/onap/aai/validation/reader/InstanceEntityReader.java b/src/main/java/org/onap/aai/validation/reader/InstanceEntityReader.java new file mode 100644 index 0000000..0b9fbed --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/InstanceEntityReader.java @@ -0,0 +1,75 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader; + +import com.google.gson.JsonElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.modeldriven.validator.InstanceReader; +import org.onap.aai.validation.modeldriven.validator.InstanceReader.InstanceEntity; +import org.onap.aai.validation.reader.data.EntityId; + +/** + * Entity reader implemented using the model-driven instance reader. + * + */ +public class InstanceEntityReader implements EntityReader { + + private InstanceReader reader; + + /** + * @param instanceReader + */ + public InstanceEntityReader(InstanceReader instanceReader) { + this.reader = instanceReader; + } + + @Override + public Object getObject(String json, String attribute) throws ValidationServiceException { + throw new ValidationServiceException(ValidationServiceError.INSTANCE_READER_NO_INSTANCE, "Not implemented"); + } + + @Override + public List<EntityId> getIds(String json, String type) throws ValidationServiceException { + List<EntityId> ids = new ArrayList<>(); + + InstanceEntity entity = reader.getNamedQueryEntity(json); + + List<String> primaryKeys = reader.getOxmReader().getPrimaryKeys(entity.getEntityType()); + + if (primaryKeys != null && !primaryKeys.isEmpty()) { + for (String pk : primaryKeys) { + JsonElement jsonValue = entity.getObject().getAsJsonObject().get(pk); + if (jsonValue != null) { + ids.add(new EntityId(pk, jsonValue.getAsString())); + } + } + } + + return ids; + } + + @Override + public Optional<String> getResourceVersion(String json) throws ValidationServiceException { + return Optional.of(reader.getResourceVersion(json)); + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/reader/JsonReader.java b/src/main/java/org/onap/aai/validation/reader/JsonReader.java new file mode 100644 index 0000000..9d349ac --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/JsonReader.java @@ -0,0 +1,186 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.ReadContext; +import com.jayway.jsonpath.spi.json.GsonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonProvider; +import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; +import com.jayway.jsonpath.spi.mapper.MappingProvider; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; + +/** + * Reads JSON objects. Supported by the JayWay JsonPath library. + */ +public class JsonReader { + + private Configuration jsonPathConfig; + + /** + * Initialise the JSON reader. + */ + public JsonReader() { + setJsonProvider(); + this.jsonPathConfig = Configuration.builder().options(Option.SUPPRESS_EXCEPTIONS).build(); + } + + /** + * Parse the JSON. + * + * @param json + * the JSON object + * @return a {@link ReadContext} the parsed JSON. + * @throws ValidationServiceException + */ + public DocumentContext parse(String json) throws ValidationServiceException { + DocumentContext document = null; + try { + document = JsonPath.using(jsonPathConfig).parse(json); + } catch (Exception e) { + throw new ValidationServiceException(ValidationServiceError.JSON_READER_PARSE_ERROR, e); + } + return document; + } + + /** + * Gets values from JSON objects. + * + * @param json + * the JSON object + * @param path + * the path to property values. The format must comply with the JayWay JsonPath definition. + * @return a List of values found by evaluating the path. + * @throws ValidationServiceException + */ + public List<String> get(String json, String path) throws ValidationServiceException { + return getAsList(parse(json), path); + } + + /** + * Gets values from JSON objects. Used in combination with {@link JsonReader#parse(String)} it reduces the number of + * times the JSON document is parsed. + * + * @param document + * a {@link DocumentContext} object with the parsed JSON + * @param path + * the path to property values. The format must comply with the JayWay JsonPath definition. + * @return a List of values found by evaluating the path, or an empty list if no values were found + */ + public List<String> getAsList(DocumentContext document, String path) { + List<String> result = new ArrayList<>(); + JsonElement jsonElement = document.read(path); + if (jsonElement != null) { + if (jsonElement.isJsonPrimitive()) { + result.add(jsonElement.getAsString()); + } else if (jsonElement.isJsonObject()) { + result.add(jsonElement.getAsJsonObject().toString()); + } else if (jsonElement.isJsonArray()) { + for (JsonElement obj : jsonElement.getAsJsonArray()) { + Object object = jsonElementToObject(obj); + result.add(object == null ? null : object.toString()); + } + } + } + return result; + } + + /** + * Get the value(s) from the specified JSON document + * + * @param document + * a {@link DocumentContext} object with the parsed JSON + * @param path + * the path to property value(s). The format must comply with the JayWay JsonPath definition. + * @return either all the values found by evaluating the path (e.g. as an array), or a String object (only) where + * the path evaluates to a single primitive value + */ + public Object getObject(DocumentContext document, String path) { + return jsonElementToObject(document.read(path)); + } + + /** + * Convert the JSON element to a String or Array where possible, otherwise return the JSON object. + * + * @param jsonElement + * @return the jsonElement converted to a Java Object + */ + private Object jsonElementToObject(JsonElement jsonElement) { + if (jsonElement == null) { + return null; + } else if (jsonElement.isJsonPrimitive()) { + return jsonElement.getAsString(); + } else if (jsonElement.isJsonObject()) { + return jsonElement.getAsJsonObject(); + } else if (jsonElement.isJsonArray()) { + // Convert to a List for simplified handling within rules + return jsonArrayToList(jsonElement.getAsJsonArray()); + } else { + return jsonElement; + } + } + + private List<Object> jsonArrayToList(JsonArray jsonArray) { + List<Object> result = new ArrayList<>(); + for (JsonElement obj : jsonArray) { + result.add(jsonElementToObject(obj)); + } + return result; + } + + /** + * @param document + * @param path + * @return a JsonElement from the document + */ + public JsonElement getJsonElement(DocumentContext document, String path) { + return document.read(path); + } + + private void setJsonProvider() { + Configuration.setDefaults(new Configuration.Defaults() { + private final JsonProvider jsonProvider = new GsonJsonProvider(); + private final MappingProvider mappingProvider = new GsonMappingProvider(); + + @Override + public JsonProvider jsonProvider() { + return jsonProvider; + } + + @Override + public MappingProvider mappingProvider() { + return mappingProvider; + } + + @Override + public Set<Option> options() { + return EnumSet.noneOf(Option.class); + } + }); + } +} diff --git a/src/main/java/org/onap/aai/validation/reader/OxmConfigTranslator.java b/src/main/java/org/onap/aai/validation/reader/OxmConfigTranslator.java new file mode 100644 index 0000000..53be6dd --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/OxmConfigTranslator.java @@ -0,0 +1,100 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.onap.aai.setup.ConfigTranslator; +import org.onap.aai.setup.SchemaLocationsBean; +import org.onap.aai.setup.Version; + +/** + * Determine which OXM and edge rules files to return based on the latest Version + * + */ +public class OxmConfigTranslator extends ConfigTranslator { + + public OxmConfigTranslator(SchemaLocationsBean bean) { + super(bean); + } + + @Override + public Map<Version, List<String>> getNodeFiles() { + String nodeDirectory = bean.getNodeDirectory(); + if (nodeDirectory == null) { + throw new ServiceConfigurationError( + "Node(s) directory is empty in the schema location bean (" + bean.getSchemaConfigLocation() + ")"); + } + try { + return getVersionMap(Paths.get(nodeDirectory), "*_v*.xml"); + } catch (IOException e) { + throw new ServiceConfigurationError("Failed to read node(s) directory " + getPath(nodeDirectory), e); + } + } + + @Override + public Map<Version, List<String>> getEdgeFiles() { + String edgeDirectory = bean.getEdgeDirectory(); + if (edgeDirectory == null) { + throw new ServiceConfigurationError( + "Edge(s) directory is empty in the schema location bean (" + bean.getSchemaConfigLocation() + ")"); + } + try { + return getVersionMap(Paths.get(edgeDirectory), "*_v*.json"); + } catch (IOException e) { + throw new ServiceConfigurationError("Failed to read edge(s) directory " + getPath(edgeDirectory), e); + } + } + + private String getPath(String nodeDirectory) { + return Paths.get(nodeDirectory).toAbsolutePath().toString(); + } + + /** + * Creates a map containing each OXM Version and the matching OXM file path(s) + * + * @param folderPath the folder/directory containing the OXM files + * @param fileSuffix + * @return a new Map object (may be empty) + * @throws IOException if there is a problem reading the specified directory path + */ + private Map<Version, List<String>> getVersionMap(Path folderPath, String globPattern) throws IOException { + final PathMatcher filter = folderPath.getFileSystem().getPathMatcher("glob:**/" + globPattern); + try (final Stream<Path> stream = Files.list(folderPath)) { + return stream.filter(filter::matches).map(Path::toString).filter(p -> getVersionFromPath(p) != null) + .collect(Collectors.groupingBy(this::getVersionFromPath)); + } + } + + private Version getVersionFromPath(String pathName) { + String version = "V" + pathName.replaceAll("\\D+", ""); + try { + return Version.valueOf(version); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/org/onap/aai/validation/reader/OxmReader.java b/src/main/java/org/onap/aai/validation/reader/OxmReader.java new file mode 100644 index 0000000..b8343f1 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/OxmReader.java @@ -0,0 +1,99 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceConfigurationError; +import org.eclipse.persistence.internal.oxm.mappings.Descriptor; +import org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext; +import org.onap.aai.nodes.NodeIngestor; +import org.onap.aai.setup.Version; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.util.StringUtils; + +/** + * Reads the primary keys of a specific OXM resource (identified by version). + */ +public class OxmReader { + + private NodeIngestor nodeIngestor; + private Version version; + private Map<String, List<String>> primaryKeysMap = new HashMap<>(); + + public OxmReader(NodeIngestor nodeIngestor, Version version) { + this.nodeIngestor = nodeIngestor; + this.version = version; + } + + public OxmReader(NodeIngestor nodeIngestor) { + this(nodeIngestor, Version.getLatest()); + } + + /** + * Get the primary keys for a given entity type. + * + * @param entityType the name of the entity type + * @return the primary keys for the entity type + * @throws ValidationServiceException + */ + public List<String> getPrimaryKeys(String entityType) throws ValidationServiceException { + if (primaryKeysMap.isEmpty()) { + throw new ValidationServiceException(ValidationServiceError.OXM_MISSING_KEY, entityType); + } + + List<String> primaryKeys = primaryKeysMap.get(entityType); + return primaryKeys != null ? primaryKeys : Collections.emptyList(); + } + + /** + * Populate a Map of primary keys for the entity types defined in the OXM.<br> + * The primary keys are keyed by the Default Root Element. + */ + public void init() { + for (Descriptor<?, ?, ?, ?, ?, ?, ?, ?, ?, ?> descriptor : getObjectDescriptors()) { + if (!descriptor.getPrimaryKeyFieldNames().isEmpty()) { + primaryKeysMap.put(descriptor.getDefaultRootElement(), + StringUtils.stripSuffix(descriptor.getPrimaryKeyFieldNames(), "/text()")); + } + } + } + + /** + * Gets a list of descriptors from the OXM. These descriptor objects contain property information about each entity + * type. + * + * @return list of descriptors + */ + @SuppressWarnings("rawtypes") + private List<Descriptor> getObjectDescriptors() { + DynamicJAXBContext dynamicJaxbContext = nodeIngestor.getContextForVersion(version); + if (dynamicJaxbContext == null) { + throw new ServiceConfigurationError("OXM Version " + version + " was not ingested."); + } + return dynamicJaxbContext.getXMLContext().getDescriptors(); + } + + public Map<String, List<String>> getPrimaryKeysMap() { + return primaryKeysMap; + } + +} diff --git a/src/main/java/org/onap/aai/validation/reader/data/AttributeValues.java b/src/main/java/org/onap/aai/validation/reader/data/AttributeValues.java new file mode 100644 index 0000000..78665ef --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/data/AttributeValues.java @@ -0,0 +1,139 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader.data; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Stores a collection of attribute values (retrievable by name). + */ +public class AttributeValues { + + private Map<String, Object> map; + + /** + * Instantiates a new empty set of attribute values. + */ + public AttributeValues() { + this.map = new HashMap<>(); + } + + /** + * Instantiates a new set of attribute values using the provided properties map. + * + * @param map + * the attribute name/value pairs + */ + public AttributeValues(Map<String, Object> map) { + this.map = map; + } + + /** + * Instantiates a new set of attribute values comprising one name/value pair. + * + * @param key + * the attribute name + * @param value + * the attribute value + */ + public AttributeValues(String key, String value) { + this(); + this.map.put(key, value); + } + + @Override + public String toString() { + return map.toString(); + } + + /** + * + * @return the number of attributes stored + */ + public int size() { + return this.map.size(); + } + + /** + * Add an attribute name/value pair. + * + * @param key + * the attribute name + * @param value + * the attribute value + */ + public void put(String key, Object value) { + this.map.put(key, value); + } + + /** + * Add an attribute name with a collection of values. + * + * @param key + * the attribute name + * @param valueList + * the collection of attribute values + */ + public void put(String key, List<String> valueList) { + this.map.put(key, valueList); + } + + /** + * Removes the attribute + * + * @param key + * the attribute name + */ + public void remove(String key) { + this.map.remove(key); + + } + + /** + * Overwrites the attribute values, replacing them with the specified attribute to attribute values mappings + * + * @param map + * the name/values to store + */ + public void setMap(Map<String, Object> map) { + this.map = map; + } + + /** + * Gets an attribute value + * + * @param attrName + * the attribute name + * @return the object + */ + public Object get(String attrName) { + return this.map.get(attrName); + } + + /** + * Converts the attribute values into a report-friendly format + * + * @return a Map which is a copy of the AttributeValues, formatted for reporting + */ + public Map<String, Object> generateReport() { + return map; + } + +} diff --git a/src/main/java/org/onap/aai/validation/reader/data/Entity.java b/src/main/java/org/onap/aai/validation/reader/data/Entity.java new file mode 100644 index 0000000..415298f --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/data/Entity.java @@ -0,0 +1,135 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.reader.EntityReader; + +/** + * An A&AI entity. + */ +public class Entity { + + private String json; + private String type; + private EntityReader reader; + private List<EntityId> ids = new ArrayList<>(); + private Optional<String> resourceVersion = Optional.empty(); + private String entityLink; + + /** + * + * @param json + * the full entity JSON supplied in the event + * @param entityType + * the entity type + * @param entityLink + * @param entityReader + * an {@link EntityReader} + */ + public Entity(final String json, final String entityType, final String entityLink, final EntityReader entityReader) { + this.json = json; + this.type = entityType; + this.entityLink = entityLink; + this.reader = entityReader; + } + + /** + * Get the full entity JSON. + * + * @return the entity JSON + */ + public String getJson() { + return json; + } + + /** + * Get the entity type. + * + * @return the entity type + */ + public String getType() { + return type; + } + + /** + * Get the entity identifiers. More than one identifier may be provided for composite keys. + * + * @return a list of entity identifiers + * @throws ValidationServiceException + */ + public List<EntityId> getIds() throws ValidationServiceException { + if (ids.isEmpty()) { + ids = reader.getIds(getJson(), getType()); + } + return ids; + } + + /** + * Get the resource version. + * + * @return the resource version + * @throws ValidationServiceException + */ + public Optional<String> getResourceVersion() throws ValidationServiceException { + if (!resourceVersion.isPresent()) { + resourceVersion = reader.getResourceVersion(getJson()); + } + return resourceVersion; + } + + /** + * Get the event entity link. + * + * @return the event entity link + */ + public String getEntityLink() { + return entityLink; + } + + /** + * Get collection of attributes + * + * @param attributes + * the names of the attributes + * @return an {@link AttributeValues} object containing the attribute values + * @throws ValidationServiceException + */ + public AttributeValues getAttributeValues(List<String> attributes) throws ValidationServiceException { + AttributeValues attributeValues = new AttributeValues(); + + if (attributes == null || attributes.isEmpty()) { + return attributeValues; + } + + for (String attribute : attributes) { + attributeValues.put(attribute, reader.getObject(getJson(), attribute)); + } + + return attributeValues; + } + + @Override + public String toString() { + return json; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/reader/data/EntityId.java b/src/main/java/org/onap/aai/validation/reader/data/EntityId.java new file mode 100644 index 0000000..83c1bf6 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/reader/data/EntityId.java @@ -0,0 +1,91 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.reader.data; + +import java.util.Objects; +import org.apache.commons.lang3.builder.EqualsBuilder; + +/** + * Describes an identifier for an entity. + */ +public class EntityId { + + private String primaryKey; + private String value; + + /** + * Construct an entity Id + */ + public EntityId() { + // Deliberately empty + } + + /** + * Construct an entity Id (key-value pair) + * + * @param primaryKey + * @param value + */ + public EntityId(String primaryKey, String value) { + this.primaryKey = primaryKey; + this.value = value; + } + + public String getPrimaryKey() { + return primaryKey; + } + + public void setPrimaryKey(String primaryKey) { + this.primaryKey = primaryKey; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public int hashCode() { + return Objects.hash(this.primaryKey, this.value); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof EntityId)) { + return false; + } else if (obj == this) { + return true; + } + EntityId rhs = (EntityId) obj; + // @formatter:off + return new EqualsBuilder() + .append(primaryKey, rhs.primaryKey) + .append(value, rhs.value) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return primaryKey + "=" + value; + } + +} diff --git a/src/main/java/org/onap/aai/validation/result/ValidationResult.java b/src/main/java/org/onap/aai/validation/result/ValidationResult.java new file mode 100644 index 0000000..b632a0f --- /dev/null +++ b/src/main/java/org/onap/aai/validation/result/ValidationResult.java @@ -0,0 +1,244 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.result; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.annotations.Expose; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.reader.data.Entity; +import org.onap.aai.validation.reader.data.EntityId; +import org.onap.aai.validation.util.JsonUtil; + +/** + * The result of an instance validation. This can include zero or more {@link Violation} objects. + */ +public class ValidationResult { + + @Expose + private String validationId; + + @Expose + private String validationTimestamp; + + @Expose + private JsonElement entityId; + + @Expose + private String entityType; + + @Expose + private String entityLink; + + @Expose + private String resourceVersion; + + @Expose + private JsonElement entity; + + + @Expose + private List<Violation> violations = new ArrayList<>(); + + /** + * Create the validation payload initialised with an event identifier and a timestamp. + * + * @param entity + * @throws ValidationServiceException + */ + public ValidationResult(Entity entity) throws ValidationServiceException { + this.validationId = UUID.randomUUID().toString(); + this.validationTimestamp = + DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssX").withZone(ZoneOffset.UTC).format(Instant.now()); + this.entityId = new JsonObject(); + for (EntityId id : entity.getIds()) { + this.entityId.getAsJsonObject().addProperty(id.getPrimaryKey(), id.getValue()); + } + this.entityType = entity.getType(); + this.entityLink = entity.getEntityLink(); + this.resourceVersion = entity.getResourceVersion().orElse(null); + this.entity = entity.getJson()!=null ?new JsonParser().parse(entity.getJson()): new JsonObject(); + } + + + /** + * Add a validation violation. + * + * @param violation a single {@link Violation} to add to the validation result + */ + public void addViolation(Violation violation) { + this.violations.add(violation); + } + + /** + * Add a list of validation violations. + * + * @param violations a List of {@link Violation} objects to add to the validation result + */ + public void addViolations(List<Violation> violations) { + this.violations.addAll(violations); + } + + public String getValidationId() { + return validationId; + } + + public void setValidationId(String eventId) { + this.validationId = eventId; + } + + public String getValidationTimestamp() { + return validationTimestamp; + } + + public void setValidationTimestamp(String timestamp) { + this.validationTimestamp = timestamp; + } + + public JsonElement getEntityId() { + return entityId; + } + + public void setEntityId(JsonElement entityId) { + this.entityId = entityId; + } + + public String getEntityType() { + return entityType; + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + + public String getEntityLink() { + return entityLink; + } + + public void setEntityLink(String uri) { + this.entityLink = uri; + } + + public String getResourceVersion() { + return resourceVersion; + } + + public void setResourceVersion(String resourceVersion) { + this.resourceVersion = resourceVersion; + } + + public JsonElement getEntity() { + return entity; + } + + public void setEntity(JsonElement entity) { + this.entity = entity; + } + + public List<Violation> getViolations() { + return violations; + } + + public void setViolations(List<Violation> violations) { + this.violations = violations; + } + + @Override + public int hashCode() { + return Objects.hash(this.entityId, this.entityLink, this.entityType, this.resourceVersion, this.validationId, + this.validationTimestamp, this.violations, this.entity); + } + + /* + * validationId is checked first, as this is meant to be unique + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ValidationResult)) { + return false; + } else if (obj == this) { + return true; + } + ValidationResult rhs = (ValidationResult) obj; + // @formatter:off + return new EqualsBuilder() + .append(entityId, rhs.entityId) + .append(entityLink, rhs.entityLink) + .append(entityType, rhs.entityType) + .append(resourceVersion, rhs.resourceVersion) + .append(validationId, rhs.validationId) + .append(validationTimestamp, rhs.validationTimestamp) + .append(violations, rhs.violations) + .append(entity, rhs.entity) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return JsonUtil.toJson(this); + } + + /** + * Create a JSON representation of the object, with each violation's validationRule omitted when it has a null value + * + * @return this object formatted as a JSON string ready for publishing + */ + public String toJson() { + return toString(); + } + + /** + * Create a new object from the JSON representation + * + * @param json representation of the Validation Result + * @return a ValidationResult object + */ + public static ValidationResult fromJson(String json) { + ValidationResult validationResult = JsonUtil.toAnnotatedClassfromJson(json, ValidationResult.class); + if (validationResult != null) { + validationResult.initialiseValues(); + } + return validationResult; + } + + /** + * Ensure that any unset fields are properly initialised. This is particularly useful when the object has been + * deserialised from a JSON string, as any missing/undefined values will not be read by the deserialiser and thus + * the corresponding fields will not be set. + */ + private void initialiseValues() { + List<Violation> violationList = getViolations(); + if (violationList != null) { + for (Violation violation : violationList) { + violation.initialiseValues(); + } + } + } +} diff --git a/src/main/java/org/onap/aai/validation/result/Violation.java b/src/main/java/org/onap/aai/validation/result/Violation.java new file mode 100644 index 0000000..29baed3 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/result/Violation.java @@ -0,0 +1,418 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.result; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.Expose; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.reader.data.Entity; +import org.onap.aai.validation.reader.data.EntityId; +import org.onap.aai.validation.util.JsonUtil; + +/** + * A validation violation. + */ +public class Violation { + + public static final String ENTITY_TYPE_PROPERTY = "entityType"; + public static final String ENTITY_ID_PROPERTY = "entityId"; + public static final String ENTITY_MODELNAME_PROPERTY = "modelName"; + + @Expose + private final String violationId; + + @Expose + private String modelName; + + @Expose + private final String category; + + @Expose + private final String severity; + + @Expose + private final String violationType; + + /** + * The rule name (not applicable for model-driven) is not final as the value may be set to null via deserialisation + * and we will need to update the field. + */ + @Expose + private Optional<String> validationRule; + + @Expose + private final JsonElement violationDetails; + + @Expose + private String errorMessage; + + /** + * rule-based or model-driven? + */ + public enum ViolationType { + NONE, RULE, MODEL + } + + /** + * Builder for a Violation. + */ + public static class Builder { + + private final MessageDigest messageDigest; + private final JsonElement entityId; + private final String entityType; + private final String entityLink; + private final String resourceVersion; + private String category = null; + private String severity = null; + private String violationType = null; + private Optional<String> validationRule = Optional.empty(); + private JsonElement violationDetails = new JsonObject(); + private String errorMessage = null; + + /** + * Create a Violation Builder for the supplied entity. + * + * @param entity + * the entity + * @throws ValidationServiceException + * the validation service exception + */ + public Builder(Entity entity) throws ValidationServiceException { + this.entityId = new JsonObject(); + for (EntityId id : entity.getIds()) { + this.entityId.getAsJsonObject().addProperty(id.getPrimaryKey(), id.getValue()); + } + this.entityType = entity.getType(); + this.entityLink = entity.getEntityLink(); + this.resourceVersion = entity.getResourceVersion().orElse(null); + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new ValidationServiceException(ValidationServiceError.MESSAGE_DIGEST_ERROR, e); + } + } + + /** + * Category. + * + * @param val + * the val + * @return the builder + */ + public Builder category(String val) { + category = val; + return this; + } + + /** + * Severity. + * + * @param val + * the val + * @return the builder + */ + public Builder severity(String val) { + severity = val; + return this; + } + + /** + * Violation type. + * + * @param val + * the val + * @return the builder + */ + public Builder violationType(String val) { + violationType = val; + return this; + } + + /** + * Violation type. + * + * @param type + * the type + * @return the builder + */ + public Builder violationType(ViolationType type) { + String name = type.name(); + // Convert to Camel Case + return violationType(name.substring(0, 1).toUpperCase() + name.substring(1, name.length()).toLowerCase()); + } + + /** + * Validation rule. + * + * @param val + * the val + * @return the builder + */ + public Builder validationRule(String val) { + validationRule = Optional.ofNullable(val); + return this; + } + + /** + * Violation details. + * + * @param map + * the map + * @return the builder + */ + public Builder violationDetails(Map<String, Object> map) { + violationDetails = JsonUtil.toJsonElement(map); + return this; + } + + /** + * Error message. + * + * @param val + * the val + * @return the builder + */ + public Builder errorMessage(String val) { + errorMessage = val; + return this; + } + + /** + * Generate violation id. + * + * @return a deterministic identifier of the violation (from its details) which may be used to compare the + * equality of violations which are created at different times + * @throws ValidationServiceException + * the validation service exception + */ + public String generateViolationId() throws ValidationServiceException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + StringBuilder result = new StringBuilder(); + writeObjectToStream(baos); + messageDigest.reset(); // Not strictly needed as digest() will cause a reset + for (byte byt : messageDigest.digest(baos.toByteArray())) { + result.append(Integer.toString((byt & 0xff) + 0x100, 16).substring(1)); + } + return result.toString(); + } catch (IOException e) { + throw new ValidationServiceException(ValidationServiceError.JSON_READER_PARSE_ERROR, e); + } + } + + /** + * Builds the. + * + * @return a new Violation object + * @throws ValidationServiceException + * the validation service exception + */ + public Violation build() throws ValidationServiceException { + return new Violation(this); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Builder [entityId=" + entityId + ", entityType=" + entityType + ", entityLink=" + entityLink + ", resourceVersion=" + resourceVersion + + ", category=" + category + ", severity=" + severity + ", violationType=" + violationType + ", validationRule=" + validationRule + + ", violationDetails=" + violationDetails + ", errorMessage=" + errorMessage + "]"; + } + + /** + * Stream all fields that are required for generating a deterministic violation ID field. Note that + * resourceVersion is not included as this is allowed to vary + * + * @param baos + * the byte array output stream + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private void writeObjectToStream(ByteArrayOutputStream baos) throws IOException { + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(this.category); + oos.writeObject(this.entityId.toString()); + oos.writeObject(this.entityType); + oos.writeObject(this.entityLink); + oos.writeObject(this.severity); + oos.writeObject(this.validationRule.toString()); + oos.writeObject(this.violationDetails.toString()); + oos.writeObject(this.violationType); + oos.close(); + } + + } + + /** + * Instantiates a new Violation object via a builder. + * + * @param builder + * the builder storing the Violation values + * @throws ValidationServiceException + * the validation service exception + */ + private Violation(Builder builder) throws ValidationServiceException { + violationId = builder.generateViolationId(); + category = builder.category; + severity = builder.severity; + violationType = builder.violationType; + validationRule = builder.validationRule; + // Clone by serialising and deserialising! + violationDetails = JsonUtil.fromJson(JsonUtil.toJson(builder.violationDetails), JsonElement.class); + errorMessage = builder.errorMessage; + } + + /** + * Gets the violation id. + * + * @return the violation id + */ + public String getViolationId() { + return violationId; + } + + /** + * Gets the model name. + * + * @return the model name + */ + public String getModelName() { + return modelName; + } + + /** + * Sets the model name. + * + * @param modelName + * the new model name + */ + // Naughty and breaks the builder pattern but it saves a lot of faffing about. + public void setModelName(String modelName) { + this.modelName = modelName; + } + + /** + * Gets the category. + * + * @return the category + */ + public String getCategory() { + return category; + } + + /** + * Gets the severity. + * + * @return the severity + */ + public String getSeverity() { + return severity; + } + + /** + * Gets the violation type. + * + * @return the violation type + */ + public String getViolationType() { + return violationType; + } + + /** + * Gets the violation details. + * + * @return the violation details + */ + @SuppressWarnings("unchecked") + public Map<String, Object> getViolationDetails() { + return JsonUtil.toAnnotatedClassfromJson(violationDetails.getAsJsonObject().toString(), Map.class); + } + + /** + * Gets the error message. + * + * @return the error message + */ + public String getErrorMessage() { + return errorMessage; + } + + @Override + public int hashCode() { + return Objects.hash(this.modelName, this.category, this.errorMessage, this.severity, this.validationRule, this.violationDetails, this.violationId, this.violationType); + } + + /* + * A hand-crafted equivalence relation to compare two Violation objects. Note that the violationId is compared + * first, because this value is deterministically generated from the majority of the object's fields. + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Violation)) { + return false; + } else if (obj == this) { + return true; + } + Violation rhs = (Violation) obj; + // @formatter:off + return new EqualsBuilder() + .append(modelName, rhs.modelName) + .append(category, rhs.category) + .append(errorMessage, rhs.errorMessage) + .append(severity, rhs.severity) + .append(validationRule, rhs.validationRule) + .append(violationDetails, rhs.violationDetails) + .append(violationId, rhs.violationId) + .append(violationType, rhs.violationType) + .isEquals(); + // @formatter:on + } + + @Override + public String toString() { + return JsonUtil.toJson(this); + } + + /** + * Ensure that any unset fields are properly initialised. This is particularly useful when the object has been + * deserialised from a JSON string, as any missing/undefined values will not be read by the deserialiser and thus + * the corresponding fields will not be set. + */ + public void initialiseValues() { + if (validationRule == null) { // NOSONAR + validationRule = Optional.empty(); + } + } +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java b/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java new file mode 100644 index 0000000..476c098 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java @@ -0,0 +1,278 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.onap.aai.validation.Validator; +import org.onap.aai.validation.config.RuleIndexingConfig; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.reader.EventReader; +import org.onap.aai.validation.reader.OxmReader; +import org.onap.aai.validation.reader.data.AttributeValues; +import org.onap.aai.validation.reader.data.Entity; +import org.onap.aai.validation.result.ValidationResult; +import org.onap.aai.validation.result.Violation; +import org.onap.aai.validation.result.Violation.ViolationType; +import org.onap.aai.validation.ruledriven.configuration.EntitySection; +import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; +import org.onap.aai.validation.ruledriven.configuration.RulesConfigurationLoader; +import org.onap.aai.validation.ruledriven.rule.Rule; + +/** + * Validator using explicit rules + * + */ +public class RuleDrivenValidator implements Validator { + + private static LogHelper applicationLogger = LogHelper.INSTANCE; + + private static final String RULES_CONFIG_FILE_SUFFIX = ".groovy"; + + private Path configurationPath; + private OxmReader oxmReader; + private EventReader eventReader; + private Optional<RuleIndexingConfig> ruleIndexingConfig; + // Map of event type name against RuleManager for that event type + private Map<String, RuleManager> ruleManagers; + + + /** + * Construct a Validator that is configured using rule files + * + * @param configurationPath path to the Groovy rules files + * @param oxmReader required for validating entity types + * @param eventReader a reader for extracting entities from each event to be validated + */ + public RuleDrivenValidator(final Path configurationPath, final OxmReader oxmReader, + final EventReader eventReader, final RuleIndexingConfig ruleIndexingConfig) { + this.configurationPath = configurationPath; + this.oxmReader = oxmReader; + this.eventReader = eventReader; + this.ruleIndexingConfig = Optional.ofNullable(ruleIndexingConfig); + this.ruleManagers = null; + } + + @Override + public void initialise() throws ValidationServiceException { + ruleManagers = new HashMap<>(); + for (String eventType : getSupportedEventTypes()) { + ruleManagers.put(eventType.toLowerCase(Locale.getDefault()), loadRulesConfiguration(eventType)); + } + validateRulesConfiguration(); + } + + private RuleManager loadRulesConfiguration(String eventType) throws ValidationServiceException { + StringBuilder rulesText = new StringBuilder(); + try (Stream<Path> paths = Files.find(configurationPath.resolve(eventType), 1, + (path, basicFileAttributes) -> path.toFile().getName().matches(".*\\" + RULES_CONFIG_FILE_SUFFIX));) { + paths.forEach(appendFileContent(rulesText)); + } catch (IOException e) { + throw new ValidationServiceException(ValidationServiceError.RULES_FILE_ERROR, + configurationPath.toAbsolutePath(), e); + } + + try { + return RulesConfigurationLoader.loadConfiguration(rulesText.toString()); + } catch (GroovyConfigurationException e) { + throw new ValidationServiceException(ValidationServiceError.RULES_FILE_ERROR, e, + configurationPath.toAbsolutePath() + File.separator + "*" + RULES_CONFIG_FILE_SUFFIX); + } + } + + private void validateRulesConfiguration() throws ValidationServiceException { + for (RuleManager ruleManager : ruleManagers.values()) { + for (EntitySection entity : ruleManager.getEntities()) { + if(ruleIndexingConfig.isPresent() && ruleIndexingConfig.get().skipOxmValidation(entity.getName())) { + continue; + } + if (oxmReader != null && oxmReader.getPrimaryKeys(entity.getName()).isEmpty()) { + throw new ValidationServiceException(ValidationServiceError.OXM_MISSING_KEY, + entity.getName() + " defined in " + configurationPath.toAbsolutePath() + File.separator + + "*" + RULES_CONFIG_FILE_SUFFIX); + } + } + } + } + + /** + * Helper method to expose the configured rules. This simplifies testing of the validator. + * + * @param entityType + * @param eventType + * @return the rules defined for this entityType + */ + public List<Rule> getRulesForEntity(String entityType, String eventType) { + return ruleManagers.get(eventType.toLowerCase(Locale.getDefault())).getRulesForEntity(entityType); + } + + /* + * (non-Javadoc) + * + * @see org.onap.aai.validation.Validator#validate(java.lang.String) + */ + @Override + public List<ValidationResult> validate(String event) throws ValidationServiceException { + List<ValidationResult> validationResults = new ArrayList<>(); + + Entity entity = getEventReader().getEntity(event); + Optional<String> eventType = eventReader.getEventType(event); + List<Rule> rules = getRulesToApply(entity, eventType).orElse(null); + if (rules == null) { + throw new ValidationServiceException(ValidationServiceError.RULES_NOT_DEFINED, eventType.orElse(null)); + } + ValidationResult validationResult = new ValidationResult(entity); + Violation.Builder builder = new Violation.Builder(entity); + + for (Rule rule : rules) { + AttributeValues attributeValues = entity.getAttributeValues(rule.getAttributePaths()); + + // Execute the rule for this particular set of attribute values. + boolean valid = false; + try { + valid = rule.execute(attributeValues); + } catch (IllegalArgumentException e) { + throw new ValidationServiceException(ValidationServiceError.RULE_EXECUTION_ERROR, e, rule, + attributeValues); + } + + applicationLogger.debug(String.format("%s|%s|\"%s\"|%s", entity.getType(), entity.getIds(), rule.getName(), + valid ? "pass" : "fail")); + + if (!valid) { + //@formatter:off + Violation violation = builder + .category(rule.getErrorCategory()) + .severity(rule.getSeverity()) + .violationType(ViolationType.RULE) + .validationRule(rule.getName()) + .violationDetails(attributeValues.generateReport()) + .errorMessage(rule.getErrorMessage()) + .build(); + //@formatter:on + + validationResult.addViolation(violation); + } + } + validationResults.add(validationResult); + + return validationResults; + } + + private Optional<List<Rule>> getRulesToApply(Entity entity, Optional<String> eventType) + throws ValidationServiceException { + Optional<List<Rule>> rules = Optional.empty(); + if (eventType.isPresent()) { + Optional<RuleManager> ruleManager = getRuleManager(eventType.get().toLowerCase(Locale.getDefault())); + if (ruleManager.isPresent()) { + if (ruleIndexingConfig.isPresent() && ruleIndexingConfig.get().getIndexedEvents() != null && + ruleIndexingConfig.get().getIndexedEvents().contains(eventType.get())) { + rules = getRulesByIndex(entity, eventType.get(), ruleManager.get()); + } else { + rules = Optional.of(ruleManager.get().getRulesForEntity(entity.getType())); + } + } + } + return rules; + } + + private Optional<List<Rule>> getRulesByIndex(Entity entity, String eventType, RuleManager ruleManager) { + String rulesKey = generateKey(entity, eventType); + applicationLogger.debug(String.format("Retrieving indexed rules for key '%s'", rulesKey)); + Optional<List<Rule>> rules = Optional.of(ruleManager.getRulesForEntity(rulesKey)); + if (rules.get().isEmpty() && ruleIndexingConfig.isPresent()) { + if (ruleIndexingConfig.get().getDefaultIndexKey() == null || ruleIndexingConfig.get().getDefaultIndexKey().isEmpty()) { + applicationLogger.debug("Default index value not configured, unable to get rules"); + applicationLogger.error(ApplicationMsgs.CANNOT_VALIDATE_ERROR, eventType); + return rules; + } + String defaultKey = RuleManager.generateKey(new String[] {ruleIndexingConfig.get().getDefaultIndexKey()}); + rules = Optional.of(ruleManager.getRulesForEntity(defaultKey)); + } + return rules; + } + + private String generateKey(Entity entity, String eventType) { + if (!ruleIndexingConfig.isPresent() || ruleIndexingConfig.get().getIndexAttributes() == null || + ruleIndexingConfig.get().getIndexAttributes().isEmpty()) { + applicationLogger.debug(String.format( + "Event '%s' is configured to use indexed rules but indexing attributes are not configured", eventType)); + return ""; + } + try { + AttributeValues attributeValues = entity.getAttributeValues(ruleIndexingConfig.get().getIndexAttributes()); + applicationLogger.debug("Generating index using attributes: " + attributeValues.generateReport().toString()); + Collection<Object> values = attributeValues.generateReport().values(); + return RuleManager.generateKey(values.stream().toArray(String[]::new)); + } catch (ValidationServiceException e) { + applicationLogger.debug("Failed to retrieve index key attributes from event"); + applicationLogger.error(ApplicationMsgs.CANNOT_VALIDATE_ERROR, eventType); + return ""; + } + } + + private EventReader getEventReader() { + return this.eventReader; + } + + private Optional<RuleManager> getRuleManager(String eventType) throws ValidationServiceException { + if (ruleManagers == null) { + initialise(); + } + return Optional.ofNullable(ruleManagers.get(eventType)); + } + + /** + * Read the text content of the specified Path and append this to the specified String + * + * @param sb StringBuilder for the rule configuration text + * @return a Consumer function that appends file content + */ + private Consumer<? super Path> appendFileContent(StringBuilder sb) { + return path -> { + try { + for (String line : Files.readAllLines(path)) { + sb.append(line).append("\n"); + } + } catch (IOException e) { + applicationLogger.error(ApplicationMsgs.READ_FILE_ERROR, e, path.toString()); + } + }; + } + + private Collection<String> getSupportedEventTypes() { + String[] list = configurationPath.toFile().list((current, name) -> new File(current, name).isDirectory()); + return list == null ? Collections.emptyList() : Arrays.asList(list); + } +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/RuleManager.java b/src/main/java/org/onap/aai/validation/ruledriven/RuleManager.java new file mode 100644 index 0000000..c21ce24 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/RuleManager.java @@ -0,0 +1,91 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import org.onap.aai.validation.ruledriven.configuration.EntitySection; +import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; +import org.onap.aai.validation.ruledriven.configuration.RuleSection; +import org.onap.aai.validation.ruledriven.rule.GroovyRule; +import org.onap.aai.validation.ruledriven.rule.Rule; + +/** + * Helper class storing the relationships from entity type to rules. This class constructs the actual rules from the + * supplied configuration. + * + */ +public class RuleManager { + + private Map<String, List<Rule>> rulesMap = new LinkedHashMap<>(); + private List<EntitySection> entities; + + /** + * Create the rules for each type of entity based on the supplied configuration + * + * @param entities configuration (all entities) + * @throws InstantiationException + * @throws IllegalAccessException + * @throws GroovyConfigurationException + * @throws IOException + */ + public RuleManager(List<EntitySection> entities) + throws InstantiationException, IllegalAccessException, GroovyConfigurationException, IOException { + this.entities = entities; + for (EntitySection entity : entities) { + List<Rule> rules = new ArrayList<>(); + for (RuleSection section : entity.getRules()) { + rules.add(new GroovyRule(section)); + } + rulesMap.put(entity.getType(), rules); + } + } + + public List<EntitySection> getEntities() { + return entities; + } + + /** + * @param entityType + * @return the rules configured for this entity type + */ + public List<Rule> getRulesForEntity(String entityType) { + List<Rule> rules = rulesMap.get(entityType); + return rules == null ? Collections.emptyList() : rules; + } + + public static String generateKey(String[] indices) { + SortedSet<String> sortedIndices = new TreeSet<>(); + Collections.addAll(sortedIndices, indices); + StringBuilder sb = new StringBuilder(); + Iterator<String> iterator = sortedIndices.iterator(); + while (iterator.hasNext()) { + sb.append("["); + sb.append(iterator.next()); + sb.append("]"); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/EntitySection.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/EntitySection.java new file mode 100644 index 0000000..357ed87 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/EntitySection.java @@ -0,0 +1,101 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import org.onap.aai.validation.ruledriven.configuration.build.EntityBuilder; + +/** + * Rules Configuration: entity {} section. + */ +public class EntitySection { + + private String name; + private String type; + private final SortedSet<String> indices = new TreeSet<>(); + private final List<RuleSection> rules = new ArrayList<>(); + + /** + * Rules are specified within an entity section. + */ + public EntitySection() { + // Deliberately empty - invoked when an entity section is read from the rules DSL + } + + @Override + public String toString() { + return new EntityBuilder(this).toString(); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public void setName(String name) { + this.name = name; + } + + public void setType(String entityType) { + this.type = entityType; + } + + public void setIndices(List<String> indices) { + for(String index : indices) { + this.indices.add(index.trim()); + } + } + + public void setIndices(String[] indices) { + for(String index : indices) { + this.indices.add(index.trim()); + } + } + + public SortedSet<String> getIndices() { + return this.indices; + } + + /** + * Adds the rule. + * + * @param rule + * the rule + */ + public void addRule(RuleSection rule) { + this.rules.add(rule); + } + + /** + * <p> + * Getter for property {@link #rules}. + * </p> + * + * @return Value for property <tt>rules</tt>. + */ + public List<RuleSection> getRules() { + return this.rules; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/GroovyConfigurationException.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/GroovyConfigurationException.java new file mode 100644 index 0000000..3b1b98a --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/GroovyConfigurationException.java @@ -0,0 +1,103 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import groovy.lang.MissingMethodException; +import groovy.lang.MissingPropertyException; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.MultipleCompilationErrorsException; +import org.onap.aai.validation.exception.BaseValidationServiceException; +import org.onap.aai.validation.exception.ValidationServiceError; + +/** + * Configuration load/parse exceptions + * + */ +@SuppressWarnings("serial") +public class GroovyConfigurationException extends BaseValidationServiceException { + + private String invalidToken; // NOSONAR + private String configText; // NOSONAR + + /** + * @param text + */ + public GroovyConfigurationException(String text) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId() + ", " + text); + } + + /** + * @param text + * @param configuration + */ + public GroovyConfigurationException(String text, String configuration) { + super(text); + this.configText = configuration; + } + + /** + * @param e + * exception for a missing method + * @param configuration + */ + public GroovyConfigurationException(MissingMethodException e, String configuration) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), "Invalid keyword " + e.getMethod(), e); + setInvalidToken(e.getMethod()); + this.configText = configuration; + } + + /** + * @param e + * @param configuration + */ + public GroovyConfigurationException(MissingPropertyException e, String configuration) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), "Invalid keyword " + e.getProperty(), e); + setInvalidToken(e.getProperty()); + this.configText = configuration; + } + + /** + * @param e + */ + public GroovyConfigurationException(CompilationFailedException e) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId() + ", " + + ValidationServiceError.RULE_UNEXPECTED_TOKEN.getMessage() + "; Caused by: " + e.getMessage(), e); + } + + /** + * @param e + * @param configuration + */ + public GroovyConfigurationException(MultipleCompilationErrorsException e, String configuration) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), "", e); + this.configText = configuration; + } + + public String getInvalidToken() { + return invalidToken; + } + + public void setInvalidToken(String token) { + this.invalidToken = token; + } + + public String getConfigText() { + return configText; + } + +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/RuleSection.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RuleSection.java new file mode 100644 index 0000000..b03bcf3 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RuleSection.java @@ -0,0 +1,200 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.onap.aai.validation.ruledriven.configuration.build.RuleBuilder; + +/** + * Rules Configuration: rule {} section. + */ +public class RuleSection { + + private String name; + private boolean isGenericRule; + private String description; + private String category; + private String errorMessage; + private String type; + private String objectId; + private String severity; + private List<String> attributes = new ArrayList<>(); + private List<String> fields = new ArrayList<>(); + private String expression; + + /** + * Rules may be defined within an entity {} section. + */ + public RuleSection() { + isGenericRule = false; + } + + /** + * @param attribute + */ + public void addAttribute(String attribute) { + if (this.attributes == null) { + this.attributes = new ArrayList<>(); + } + this.attributes.add(attribute); + } + + private void addAttributeMapping(String field) { + if (this.fields == null) { + this.fields = new ArrayList<>(); + } + this.fields.add(field); + } + + private void addAttributeNames(List<String> fieldNames) { + for (String attribute : fieldNames) { + addAttributeMapping(attribute); + } + } + + private void addAttributes(List<String> attributes) { + for (String attribute : attributes) { + addAttribute(attribute); + } + } + + /** + * Make a copy of a generically defined rule so that we can tailor it for our specific entity. + * + * @param genericRule + */ + public void copyFrom(RuleSection genericRule) { + setCategory(genericRule.getCategory()); + setDescription(genericRule.getDescription()); + setSeverity(genericRule.getSeverity()); + setExpression(genericRule.getExpression()); + setErrorMessage(genericRule.getErrorMessage()); + if (genericRule.getAttributes() != null) { + if (getAttributes().isEmpty()) { + addAttributes(genericRule.getAttributes()); + } else { + // Map the attributes + addAttributeNames(genericRule.getAttributes()); + } + } + + if (getAttributes().isEmpty()) { + throw new IllegalArgumentException("No attributes defined"); + } + } + + public List<String> getAttributes() { + return new ArrayList<>(attributes); + } + + public String getCategory() { + return category; + } + + public String getDescription() { + return description; + } + + public String getErrorMessage() { + return errorMessage; + } + + public String getExpression() { + return expression; + } + + public List<String> getExpressionFieldNames() { + if (fields.size() < attributes.size()) { + return attributes; + } else { + return fields; + } + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getObjectId() { + return objectId; + } + + public String getSeverity() { + return severity; + } + + public boolean isGeneric() { + return isGenericRule; + } + + public void setAttributes(List<String> attributes) { + this.attributes = attributes; + } + + public void setAttributes(String[] attributes) { + this.attributes = new ArrayList<>(Arrays.asList(attributes)); + } + + public void setCategory(String category) { + this.category = category; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public void setIsGeneric(boolean isGenericRule) { + this.isGenericRule = isGenericRule; + } + + public void setName(String name) { + this.name = name; + } + + public void setObject(String object) { + this.type = object; + } + + public void setObjectId(String objectId) { + this.objectId = objectId; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + @Override + public String toString() { + return new RuleBuilder(this, "").toString(); + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/RulesConfigurationLoader.groovy b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RulesConfigurationLoader.groovy new file mode 100644 index 0000000..5fabde9 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RulesConfigurationLoader.groovy @@ -0,0 +1,309 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration + +import groovy.lang.Closure +import groovy.lang.DelegatesTo +import groovy.lang.ExpandoMetaClass +import java.io.File +import java.util.List +import org.onap.aai.validation.ruledriven.RuleManager + +class RulesConfigurationLoader { + + static RuleManager loadConfiguration(File dsl) { + return loadConfiguration(dsl.text) + } + + static RuleManager loadConfiguration(String dsl) throws GroovyConfigurationException { + SettingsSection globalConfiguration = new SettingsSection() + def List<EntitySection> entities = [] + def List<RuleSection> rules = [] + Script dslScript + + try { + dslScript = new GroovyShell().parse(dsl) + } catch (org.codehaus.groovy.control.MultipleCompilationErrorsException e) { + throw new GroovyConfigurationException(e, dsl) + } + + dslScript.metaClass = createEMC(dslScript.class, { ExpandoMetaClass emc -> + + emc.settings = { Closure cl -> + cl.delegate = new SettingsDelegate(globalConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + emc.entity = { Closure cl -> + EntitySection entityConfiguration = new EntitySection() + cl.delegate = new EntityDelegate(entityConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + entities.add(entityConfiguration) + } + + emc.rule = { Closure cl -> + RuleSection ruleConfiguration = new RuleSection() + cl.delegate = new RuleDelegate(ruleConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + rules.add(ruleConfiguration) + } + }) + + try { + dslScript.run() + } catch (MissingMethodException | MissingPropertyException e) { + throw new GroovyConfigurationException(e, dsl) + } + + loadGenericRules(entities, rules) + checkForDuplicateRules(rules) + + return new RuleManager(entities) + } + + static void loadGenericRules(List<EntitySection> entities, List<RuleSection> rules) { + for (entity in entities) { + for (rule in entity.getRules()) { + if (rule.isGeneric()) { + def namedRule = rules.find() { item -> + item.getName() == rule.getName() + } + if (namedRule == null) { + throw new GroovyConfigurationException("rule '" + rule.getName() + "' is not defined") + } + try { + rule.copyFrom(namedRule) + } catch (IllegalArgumentException e) { + throw new GroovyConfigurationException("rule '" + rule.getName() + "' has no attributes defined, referenced by entity '" + entity.getName() + "'") + } + } + if (rule.getExpression() == null) { + throw new GroovyConfigurationException("rule '" + rule.getName() + "' does not have an expression defined") + } + } + } + } + + static void checkForDuplicateRules(List<RuleSection> rules) { + def duplicates = rules.countBy{ rule -> rule.name }.grep{ it.value > 1 }.collect{ it.key } + + rules.each { rule -> + if (rule.name in duplicates) { + throw new GroovyConfigurationException("Generic rule '" + rule.name + "' is duplicated") + } + } + } + + static ExpandoMetaClass createEMC(Class scriptClass, Closure cl) { + ExpandoMetaClass emc = new ExpandoMetaClass(scriptClass, false) + cl(emc) + emc.initialize() + return emc + } +} + +// Parse the settings {} block +class SettingsDelegate { + private SettingsSection configuration + + SettingsDelegate(SettingsSection configuration) { + this.configuration = configuration + } + + void environment(String environment) { + this.configuration.setEnvironment environment + } +} + +// Parse an entity {} block +class EntityDelegate { + private EntitySection configuration + + EntityDelegate(EntitySection configuration) { + this.configuration = configuration + } + + void name(String name) { + this.configuration.setName name + } + + void type(String name) { + if (!configuration.name) configuration.name = name + configuration.type = name + configuration.getRules().each { rule -> + rule.setObject configuration.type + } + } + + def indexing(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=IndexingDelegate) Closure cl) { + cl.delegate = new IndexingDelegate(configuration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + def validation(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=ValidationDelegate) Closure cl) { + cl.delegate = new ValidationDelegate(configuration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} + + +// Parse an indexing {} block within an entity block +class IndexingDelegate { + private EntitySection configuration + + IndexingDelegate(EntitySection configuration) { + this.configuration = configuration + } + + void indices(String... indices) { + this.configuration.setIndices indices + def index = RuleManager.generateKey(indices) + this.configuration.type = index + this.configuration.getRules().each { rule -> + rule.setObject configuration.type + } + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} + +// Parse a validation {} block within an entity block +class ValidationDelegate { + private EntitySection configuration + private String id + + ValidationDelegate(EntitySection configuration) { + this.configuration = configuration + } + + void useRule(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=UseRuleDelegate) Closure cl) { + cl.delegate = new UseRuleDelegate(configuration, configuration.type, id) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + void rule(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=RuleDelegate) Closure cl) { + RuleSection ruleConfiguration = new RuleSection() + ruleConfiguration.setObject configuration.type + ruleConfiguration.setObjectId this.id + cl.delegate = new RuleDelegate(ruleConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + configuration.addRule(ruleConfiguration) + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } +} + +// Parse a rule {} block +class RuleDelegate { + private RuleSection configuration + + RuleDelegate(RuleSection configuration) { + this.configuration = configuration + } + + void name(String name) { + this.configuration.setName name + } + + void category(String category) { + this.configuration.setCategory category + } + + void description(String description) { + this.configuration.setDescription description + } + + void errorText(String text) { + this.configuration.setErrorMessage text + } + + void severity(String severity) { + this.configuration.setSeverity severity + } + + void attributes(String... attributesList) { + this.configuration.setAttributes attributesList + } + + void validate(String validate) { + this.configuration.setExpression validate + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} + +class UseRuleDelegate { + private EntitySection configuration + private String objectName + private String id + private RuleSection ruleConfig + + UseRuleDelegate(EntitySection configuration, String objectName, String id) { + this.configuration = configuration + this.objectName = objectName + this.id = id + } + + void name(String name) { + ruleConfig = new RuleSection() + ruleConfig.setIsGeneric true; + ruleConfig.setName name; + ruleConfig.setObject this.objectName + ruleConfig.setObjectId this.id + this.configuration.addRule ruleConfig + } + + void attributes(String[] attributes) { + if (ruleConfig.attributes.empty) { + attributes.each {ruleConfig.addAttribute it} + } + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/SettingsSection.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/SettingsSection.java new file mode 100644 index 0000000..d59d07d --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/SettingsSection.java @@ -0,0 +1,48 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import org.onap.aai.validation.ruledriven.configuration.build.EntityBuilder; + +/** + * Parse the settings configuration block. + */ +public class SettingsSection { + + private String environment; + + public SettingsSection() { + // Deliberately empty + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(new EntityBuilder()); + return sb.toString(); + } + + public String getEnvironment() { + return environment; + } + + public void setEnvironment(String environment) { + this.environment = environment; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ContentBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ContentBuilder.java new file mode 100644 index 0000000..320e29e --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ContentBuilder.java @@ -0,0 +1,198 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; + + +/** + * Helper class for building internal configuration settings. + */ +public class ContentBuilder { + + private final String contentName; + protected List<ContentBuilder> items = new ArrayList<>(); + private StringBuilder content = new StringBuilder(); + protected String indent = ""; + + /** + * Construct an empty section. + */ + public ContentBuilder() { + this.contentName = null; + } + + /** + * Instantiates a new content builder. + * + * @param contentName + * the content name + */ + public ContentBuilder(String contentName) { + this.contentName = contentName; + } + + @Override + public String toString() { + return build(); + } + + /** + * Builds the configuration section + * + * @return the configuration as a string + */ + public String build() { + StringBuilder sb = new StringBuilder(); + appendPrefix(sb); + // Build child items + for (ContentBuilder item : items) { + sb.append(item.build()); + } + // Any ad-hoc content goes here + sb.append(content); + appendSuffix(sb); + return sb.toString(); + } + + /** + * Adds the content. + * + * @param item + * the item + */ + public void addContent(ContentBuilder item) { + items.add(item); + } + + /** + * Adds the content. + * + * @param stringContent + * the string content + */ + public void addContent(String stringContent) { + content.append(stringContent); + } + + /** + * Append content as a new line. + * + * @param stringContent + * the string + */ + public void appendLine(String stringContent) { + content.append(indent).append(stringContent).append(System.lineSeparator()); + } + + /** + * Append properties. + * + * @param props + * the props + */ + public void appendProperties(Properties props) { + @SuppressWarnings("unchecked") + Enumeration<String> e = (Enumeration<String>) props.propertyNames(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + appendValue(key, props.get(key)); + } + } + + /** + * Append value. + * + * @param key + * the key + * @param value + * the value + * @return + */ + public ContentBuilder appendValue(String key, Object value) { + if (value == null) { + return this; + } + + addContent(indent + "\t" + key + " "); + + if (value instanceof String) { + addStringValue((String) value); + } else if (value instanceof Number) { + addStringValue(value.toString()); + } else if (value instanceof List<?>) { + boolean first = true; + for (Object element : (List<?>) value) { + if (!first) { + addContent(", "); + } + addStringValue((String) element); + first = false; + } + } else if (value instanceof Properties) { + appendLine("{"); + appendProperties((Properties) value); + appendLine("}"); + } else { + throw new IllegalArgumentException(key); + } + addContent(System.lineSeparator()); + + return this; + } + + /** + * Adds the string value. + * + * @param value + * the value + */ + private void addStringValue(String value) { + addContent("'" + value + "'"); + } + + /** + * Append suffix. + * + * @param sb + * the sb + */ + private void appendSuffix(StringBuilder sb) { + if (contentName != null) { + sb.append(indent).append("}").append(System.lineSeparator()); + } + } + + /** + * Append prefix. + * + * @param sb + * the sb + */ + private void appendPrefix(StringBuilder sb) { + if (contentName != null) { + sb.append(indent); + sb.append(contentName); + sb.append(" {").append(System.lineSeparator()); + } + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/EntityBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/EntityBuilder.java new file mode 100644 index 0000000..166ae50 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/EntityBuilder.java @@ -0,0 +1,70 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import java.util.Properties; +import org.onap.aai.validation.ruledriven.configuration.EntitySection; +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Builder for entity config section + * + */ +public class EntityBuilder extends ContentBuilder { + + /** + * Create an empty entity section + */ + public EntityBuilder() { + super("entity"); + } + + /** + * @param auditConfiguration + */ + public EntityBuilder(EntitySection auditConfiguration) { + this(); + appendValue("name", auditConfiguration.getName()); + + for (RuleSection rule : auditConfiguration.getRules()) { + addContent(new ValidationBuilder(rule).toString()); + } + } + + /** + * Add an empty validation section to this entity + * + * @return + */ + public ValidationBuilder validation() { + ValidationBuilder item = new ValidationBuilder(); + addContent(item); + return item; + } + + /** + * @param props + * @return + */ + public ValidationBuilder validation(Properties props) { + ValidationBuilder item = validation(); + item.appendProperties(props); + return item; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/RuleBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/RuleBuilder.java new file mode 100644 index 0000000..07fb5b4 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/RuleBuilder.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Builder for rule config section. + */ +public class RuleBuilder extends ContentBuilder { + + /** + * Instantiates a new rule builder. + */ + public RuleBuilder() { + super("rule"); + } + + /** + * Instantiates a new rule builder using the existing rule configuration + * + * @param ruleConfig + * the rule configuration to clone + * @param indent + * the indent/prefix for the section + */ + public RuleBuilder(RuleSection ruleConfig, String indent) { + this(); + this.indent = indent; + if (ruleConfig.isGeneric()) { + appendLine(indent + "\t// Generic Rule"); + } + appendValue("name", ruleConfig.getName()); + appendValue("category", ruleConfig.getCategory()); + appendValue("description", ruleConfig.getDescription()); + appendValue("severity", ruleConfig.getSeverity()); + if (ruleConfig.isGeneric()) { + appendLine(indent + "\t// Passing " + ruleConfig.getAttributes()); + } + appendValue("attributes", ruleConfig.getExpressionFieldNames()); + appendValue("validate", ruleConfig.getExpression()); + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/UseRuleBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/UseRuleBuilder.java new file mode 100644 index 0000000..fe72bac --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/UseRuleBuilder.java @@ -0,0 +1,46 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Content Builder for a useRule section + * + */ +public class UseRuleBuilder extends ContentBuilder { + + /** + * Build a useRule section + */ + public UseRuleBuilder() { + super("useRule"); + } + + /** + * @param ruleConfig + * @param indent + */ + public UseRuleBuilder(RuleSection ruleConfig, String indent) { + this(); + this.indent = indent; + appendValue("name", ruleConfig.getName()); + appendValue("attributes", ruleConfig.getAttributes()); + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ValidationBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ValidationBuilder.java new file mode 100644 index 0000000..44f3c4c --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ValidationBuilder.java @@ -0,0 +1,78 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Builder for validation config section + * + */ +public class ValidationBuilder extends ContentBuilder { + /** + * Create an empty validation section + */ + public ValidationBuilder() { + super("validation"); + indent = "\t"; + } + + /** + * Create a validation section using the existing rule configuration + * + * @param ruleConfig + */ + public ValidationBuilder(RuleSection ruleConfig) { + this(); + appendValue("object", ruleConfig.getType()); + appendValue("objectId", ruleConfig.getObjectId()); + if (ruleConfig.isGeneric()) { + addContent(new UseRuleBuilder(ruleConfig, indent + "\t").toString()); + } else { + addContent(new RuleBuilder(ruleConfig, indent + "\t").toString()); + } + } + + /** + * Add a useRule section + * + * @param name + * @return the new useRule section + */ + public UseRuleBuilder useRule(String name) { + UseRuleBuilder item = new UseRuleBuilder(); + item.indent = "\t\t"; + item.appendValue("name", name); + addContent(item); + return item; + } + + /** + * Add a rule section comprising the specified name item + * + * @param name + * @return + */ + public RuleBuilder rule(String name) { + RuleBuilder item = new RuleBuilder(); + item.indent = "\t\t"; + item.appendValue("name", name); + addContent(item); + return item; + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java b/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java new file mode 100644 index 0000000..b151f6b --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java @@ -0,0 +1,323 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.rule; + +import groovy.lang.GroovyClassLoader; +import groovy.lang.GroovyObject; +import groovy.lang.MetaMethod; +import groovy.lang.MissingMethodException; +import groovy.lang.MissingPropertyException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.runtime.InvokerInvocationException; +import org.onap.aai.cl.api.Logger; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.reader.data.AttributeValues; +import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; +import org.onap.aai.validation.ruledriven.configuration.RuleSection; +import org.onap.aai.validation.util.StringUtils; + +/** + * Rule based on a Groovy script + * + */ +public class GroovyRule implements Rule { + + private static final Logger applicationLogger = LogHelper.INSTANCE; + + /** + * Do not allow special characters (e.g. ?) in an attribute name. Hyphens and underscores are accepted. + */ + private static final Pattern ATTRIBUTE_NAME_WHITELIST = Pattern.compile("^[a-zA-Z0-9-_.\\*\\[\\]]*$"); + private static final Pattern ATTRIBUTE_NAME_BLACKLIST = Pattern.compile("^(|null)$"); + + private String errorCategory; + private String errorMessage; + private String severity; + private List<String> attributes; + private List<String> attributePaths; // where in the JSON entity to read the attributes from + + private String methodName; + private GroovyObject groovyObject; + private List<String> originalFields; + private String originalExpression; + private String groovyExpression; // NOSONAR stored for debugging purposes + private boolean ruleIsValid = true; + private String name; + + + /** + * @param ruleConfig + * @throws InstantiationException + * @throws IllegalAccessException + * @throws IOException + * @throws GroovyConfigurationException + */ + public GroovyRule(RuleSection ruleConfig) + throws InstantiationException, IllegalAccessException, IOException, GroovyConfigurationException { + setName(ruleConfig.getName()); + setErrorCategory(ruleConfig.getCategory()); + setErrorMessage(ruleConfig.getErrorMessage()); + setSeverity(ruleConfig.getSeverity()); + setAttributes(ruleConfig.getAttributes()); + setAttributePaths(ruleConfig.getAttributes()); + + this.originalFields = new ArrayList<>(); + for (String field : ruleConfig.getExpressionFieldNames()) { + originalFields.add(StringUtils.stripPrefix(field, ".")); + } + + Class<?> groovyClass = createRule(ruleConfig.getExpressionFieldNames(), ruleConfig.getExpression()); + + if (groovyClass != null) { + groovyObject = (GroovyObject) groovyClass.newInstance(); + + for (MetaMethod method : groovyObject.getMetaClass().getMethods()) { + if (method.getName().startsWith("rule")) { + methodName = method.getName(); + } + } + + try { + executeWithSampleData(); + } catch (IllegalArgumentException e) { // NOSONAR + if (e.getCause() instanceof InvokerInvocationException + && e.getCause().getCause() instanceof MissingMethodException) { + applicationLogger + .debug("WARNING: Rule \"" + getName() + "\" does not accept \"1\" for all input values"); + } else { + ruleIsValid = false; + } + } + } else { + ruleIsValid = false; + } + } + + public boolean isValid() { + return ruleIsValid; + } + + /** + * Run the rule expression on the specified attribute values + * + * @param attributeValues + * @return + */ + @Override + public Boolean execute(AttributeValues attributeValues) { + // Obtain the values of each of the attributes to pass into the rule + List<Object> valueList = new ArrayList<>(); + for (String attrName : this.attributePaths) { + valueList.add(attributeValues.get(attrName)); + } + Object[] attrValuesArray = valueList.toArray(); + return execute(attrValuesArray); + } + + /** + * Apply the rule to some attribute(s) + * + * @param values + * + * @param groovyObject an instance/object of a Groovy class that implements one or more rule methods + * @return the Boolean result of evaluating the expression + */ + @Override + public Boolean execute(Object... values) { + Object result = null; + try { + result = groovyObject.invokeMethod(getRuleMethod(), values); + } catch (MissingPropertyException | MissingMethodException | InvokerInvocationException e) { + throw new IllegalArgumentException(e); + } catch (NullPointerException e) { + throw new IllegalArgumentException("Argument is null", e); + } + + if (result instanceof Number) { + return !result.equals(0); + } else { + return (Boolean) result; + } + } + + @Override + public String toString() { + return "GroovyRule \"" + name + "\" " + attributePaths + " -> " + originalFields + " {" + originalExpression + + "}"; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getErrorCategory() { + return errorCategory; + } + + public void setErrorCategory(String errorCategory) { + this.errorCategory = errorCategory; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + @Override + public List<String> getAttributePaths() { + return attributePaths; + } + + private String getRuleMethod() { + return methodName; + } + + private void setAttributePaths(List<String> attributePaths) { + this.attributePaths = attributePaths; + } + + private void setAttributes(List<String> attributes) { + this.attributes = new ArrayList<>(); + for (String attribute : attributes) { + // Strip any prefixes containing the . character + this.attributes.add(attribute.substring(attribute.lastIndexOf('.') + 1)); + } + } + + /** + * @param fields + * @param expression + * @return + * @throws IOException + * @throws GroovyConfigurationException + */ + private Class<?> createRule(List<String> fields, String expression) + throws IOException, GroovyConfigurationException { + originalExpression = expression; + groovyExpression = expression; + String methodParams = ""; + + int i = 1; + for (String attribute : fields) { + if (isValidAttributeName(attribute)) { + String fieldName = "field" + i++; + methodParams = appendParameter(methodParams, fieldName); + // Strip any prefixes from the attribute name in case of JayWay expression attributes. + attribute = StringUtils.stripPrefix(attribute, "."); + String regex = "\\b" + attribute + "\\b(?!\\()"; + groovyExpression = groovyExpression.replaceAll(regex, fieldName); + } else { + ruleIsValid = false; + return null; + } + } + + return loadGroovyClass("def rule(" + methodParams + ") {" + groovyExpression + "}"); + } + + private String appendParameter(String methodParams, String fieldName) { + StringBuilder newParams = new StringBuilder(); + if (methodParams.length() > 0) { + newParams.append(methodParams).append(", "); + } + newParams.append("Object ").append(fieldName); + return newParams.toString(); + } + + private boolean isValidAttributeName(String attributeName) { + if (ATTRIBUTE_NAME_BLACKLIST.matcher(attributeName).matches()) { + return false; + } + + if (!ATTRIBUTE_NAME_WHITELIST.matcher(attributeName).matches()) { + return false; + } + + // Make sure that the attribute name is NOT purely a number + try (Scanner sc = new Scanner(attributeName.trim())) { + if (sc.hasNextInt()) { + sc.nextInt(); // Consume the integer + return sc.hasNext(); // Name is valid if there is more content + } + } + + return true; // Not an integer + } + + private void executeWithSampleData() { + Object[] values = new Object[attributes.size()]; + int i = 0; + for (String attribute : attributes) { + if (attribute.contains("[*]")) { + values[i++] = Arrays.asList("{}"); + } else { + values[i++] = "1"; + } + } + execute(values); + } + + /** + * Load and parse a Groovy script to create an anonymous class + * + * @param script a file containing the Groovy scripting language + * @return the Java Class for accessing the Groovy methods + * @throws IOException + * @throws GroovyConfigurationException + */ + @SuppressWarnings("rawtypes") + private static Class loadGroovyClass(String expression) throws IOException, GroovyConfigurationException { + ClassLoader parent = GroovyRule.class.getClassLoader(); + GroovyClassLoader loader = new GroovyClassLoader(parent); + Class groovyClass; + try { + groovyClass = loader.parseClass(expression); + } catch (CompilationFailedException e) { + throw new GroovyConfigurationException(e); + } finally { + loader.close(); + } + return groovyClass; + } + +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java b/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java new file mode 100644 index 0000000..1196db0 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java @@ -0,0 +1,81 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.rule; + +import java.util.List; +import org.onap.aai.validation.reader.data.AttributeValues; + +/** + * A rule that accepts one or more attributes and returns a Boolean result (when executed) + */ +public interface Rule { + + /** + * Gets the name of the rule + * + * @return the name + */ + String getName(); + + /** + * Gets the error message. + * + * @return the error message + */ + String getErrorMessage(); + + /** + * Gets the error category. + * + * @return the error category + */ + String getErrorCategory(); + + /** + * Gets the severity. + * + * @return the severity + */ + String getSeverity(); + + /** + * Gets the paths to the attributes to pass to the rule + * + * @return the attribute paths + */ + List<String> getAttributePaths(); + + /** + * Execute the rule. + * + * @param values + * the attribute values to pass to the rule + * @return a boolean representing the rule evaluation (meaning success/failure) + */ + Boolean execute(AttributeValues values); + + /** + * Execute the rule. + * + * @param values + * the attribute values to pass to the rule + * @return a boolean representing the rule evaluation (meaning success/failure) + */ + Boolean execute(Object... values); + +} diff --git a/src/main/java/org/onap/aai/validation/services/EventPollingService.java b/src/main/java/org/onap/aai/validation/services/EventPollingService.java new file mode 100644 index 0000000..8b38031 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/services/EventPollingService.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.services; + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.inject.Inject; + +import org.springframework.stereotype.Service; + +import org.onap.aai.event.client.DMaaPEventConsumer; +import org.onap.aai.validation.config.TopicConfig; +import org.onap.aai.validation.config.TopicConfig.Topic; +import org.onap.aai.validation.controller.ValidationController; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.logging.LogHelper.MdcParameter; +import com.google.common.collect.Iterables; + +/** + * Event Polling Service + * + */ +@Service +public class EventPollingService implements Runnable { + + private static final LogHelper applicationLogger = LogHelper.INSTANCE; + + private List<DMaaPEventConsumer> consumers; + + private ValidationController validationController; + + /** + * Instantiates an EventPollingService instance using the supplied configuration. + * + * @param topicConfig + * @throws ValidationServiceException + */ + @Inject + public EventPollingService(TopicConfig topicConfig) throws ValidationServiceException { + consumers = new ArrayList<>(); + for (Topic topic : topicConfig.getConsumerTopics()) { + try { + consumers.add(new DMaaPEventConsumer(topic.getHost(), topic.getName(), topic.getUsername(), + topic.getPassword(), topic.getConsumerGroup(), topic.getConsumerId(), + DMaaPEventConsumer.DEFAULT_MESSAGE_WAIT_TIMEOUT, DMaaPEventConsumer.DEFAULT_MESSAGE_LIMIT, + topic.getTransportType())); + } catch (MalformedURLException e) { + throw new ValidationServiceException(ValidationServiceError.EVENT_CLIENT_CONSUMER_INIT_ERROR, e); + } + } + } + + @Override + public void run() { + applicationLogger.info(ApplicationMsgs.POLL_EVENTS); + try { + for (DMaaPEventConsumer consumer : consumers) { + for (String event : consumeEvents(consumer)) { + // The event does not have a transaction ID so create one for logging purposes + applicationLogger.setContextValue(MdcParameter.REQUEST_ID, UUID.randomUUID().toString()); + validationController.execute(event, "topic"); + } + } + } catch (Exception e) { + // This could be a temporary issue, so the exception is swallowed + applicationLogger.error(ApplicationMsgs.INVOKE_EVENT_CONSUMER_ERROR, e); + } catch (Throwable t) { // NOSONAR + // E.g. We may catch an IllegalArgumentException caused by invalid configuration + applicationLogger.error(ApplicationMsgs.INVOKE_EVENT_CONSUMER_ERROR, t); + + // Add these details to the status report available via the controller + validationController.recordThrowable(t); + + // For non IO exceptions, rethrow in order to cause the executor to stop scheduling + throw t; + } finally { + applicationLogger.clearContextValue(MdcParameter.REQUEST_ID); + } + } + + /* Getters and Setters for Spring injection */ + + public ValidationController getValidationController() { + return validationController; + } + + public void setValidationController(ValidationController validationController) throws ValidationServiceException { + this.validationController = validationController; + // Instruct the validation controller to load and validate its configuration + this.validationController.initialise(); + } + + private Iterable<String> consumeEvents(DMaaPEventConsumer consumer) throws Exception { + applicationLogger.clearContextValue(MdcParameter.REQUEST_ID); + applicationLogger.debug("Querying consumer " + consumer); + Iterable<String> events = consumer.consume(); + applicationLogger.info(ApplicationMsgs.NUMBER_OF_MESSAGES_CONSUMED, Integer.toString(Iterables.size(events))); + return events; + } +} diff --git a/src/main/java/org/onap/aai/validation/services/InfoService.java b/src/main/java/org/onap/aai/validation/services/InfoService.java new file mode 100644 index 0000000..5f2b625 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/services/InfoService.java @@ -0,0 +1,57 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.services; + +import org.onap.aai.validation.controller.ValidationController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Information Service for the Validation Controller. Return status details to the caller. + * + */ +@RestController +@RequestMapping("/services/validation-service/v1/core/core-service") +public class InfoService { + + @Autowired + private ValidationController validationController; + + public ValidationController getValidationController() { + return validationController; + } + + public void setValidationController(ValidationController validationController) { + this.validationController = validationController; + } + + /** + * @param format is an optional setting - html requests an HTML format + * @return a formatted status report + */ + @RequestMapping(value = "/info", method = RequestMethod.GET, produces = "text/plain") + @ResponseBody + public String getInfo() { + validationController.incrementInfoCount(); + return "Status: Up\n" + validationController.statusReport().toString() + "\n"; + } + +} diff --git a/src/main/java/org/onap/aai/validation/services/RequestHeaders.java b/src/main/java/org/onap/aai/validation/services/RequestHeaders.java new file mode 100644 index 0000000..1d12600 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/services/RequestHeaders.java @@ -0,0 +1,53 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.services; + +import org.springframework.http.HttpHeaders; + +/** + * Bean to represent the ONAP request/transaction IDs required for EELF logging. + * + */ +public class RequestHeaders { + + // ONAP request ID a.k.a. transaction ID or correlation ID + public static final String HEADER_REQUEST_ID = "X-ECOMP-RequestID"; + public static final String HEADER_SERVICE_INSTANCE_ID = "X-ECOMP-ServiceInstanceID"; + + private String requestId; + private String instanceId; + + public RequestHeaders(HttpHeaders headers) { + requestId = headers.getFirst(RequestHeaders.HEADER_REQUEST_ID); + instanceId = headers.getFirst(RequestHeaders.HEADER_SERVICE_INSTANCE_ID); + } + + public String getRequestId() { + return requestId; + } + + public String getInstanceId() { + return instanceId; + } + + @Override + public String toString() { + return "RequestHeaders [requestId=" + requestId + ", instanceId=" + instanceId + "]"; + } + +} diff --git a/src/main/java/org/onap/aai/validation/services/ValidateService.java b/src/main/java/org/onap/aai/validation/services/ValidateService.java new file mode 100644 index 0000000..4ef5e7f --- /dev/null +++ b/src/main/java/org/onap/aai/validation/services/ValidateService.java @@ -0,0 +1,77 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.services; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Validates an event (containing details of an entity) and synchronously returns the details of the validation + * result(s) including any violations that were detected. Note that in the current version there is expected to be one + * and only one validation result per event. + */ +@RestController +@RequestMapping("/services/validation-service/v1/app") +public interface ValidateService { // NOSONAR + /** + * Validate an event and, if successful, return the result(s) in JSON format. Note that not every event is validated + * and so the set of results may be empty. In the event of an error/exception then plain text is returned containing + * the exception details. + * + * <h4>The HTTP message body must be comprised of a single JSON object containing the following members (nested JSON + * elements)</h4> <B>event-header</B> a JSON object which contains the following members: + * <ul> + * <li><B>domain</B> the value must match with the event domain expected by the Validation Service + * <li><B>action</B> the value must not be present in the list of excluded event actions (default list: DELETE)</li> + * <li><B>event-type</B> the value must match with one of the expected event types (either for rule-driven or for + * model-driven validation)</li> + * <li><B>top-entity-type</B> indicating the type of the entity member (see below)</li> + * <li><B>entity-type</B> the value must match with an A&AI entity defined by the OXM model. This value identifies + * the object to be validated</li> + * <li><B>entity-link</B> the value indicates the source of the entity. This is expected to begin with the text + * https://host/aai/vX/</li> + * </ul> + * <B> entity</B> a JSON object representing the top-level entity. This object must contain the single entity + * instance to be validated, either + * <ul> + * <li>contained by a parent entity of a different type (indicated by top-entity-type)</li> + * <li>or as a stand-alone JSON object (entity-type will have the same value as top-entity-type)</li> + * </ul> + * + * @param event a JSON object representing an event + * @return an HTTP Response containing either a JSON array of ValidationResult objects or a plain-text error message + * + * @responseMessage 200 Success + * @responseMessage 400 Bad Request + * @responseMessage 500 Internal Server Error + * + */ + @RequestMapping(value = "/validate", method = RequestMethod.POST, produces = {"application/json", "text/plain"}) + @ResponseBody + public ResponseEntity<String> validate(@RequestHeader HttpHeaders headers, HttpServletRequest servletRequest, + @RequestBody String event); + + public ResponseEntity<String> validate(String event); +} diff --git a/src/main/java/org/onap/aai/validation/services/ValidateServiceImpl.java b/src/main/java/org/onap/aai/validation/services/ValidateServiceImpl.java new file mode 100644 index 0000000..54f8b5a --- /dev/null +++ b/src/main/java/org/onap/aai/validation/services/ValidateServiceImpl.java @@ -0,0 +1,160 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.services; + +import java.util.Optional; +import java.util.UUID; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import org.onap.aai.auth.AAIMicroServiceAuth; +import org.onap.aai.validation.controller.ValidationController; +import org.onap.aai.validation.controller.ValidationController.Result; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.logging.LogHelper.MdcParameter; +import org.onap.aai.validation.logging.LogHelper.StatusCode; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Validation Service HTTP interface implementation. + * + */ +public class ValidateServiceImpl implements ValidateService { + + private static final LogHelper applicationLogger = LogHelper.INSTANCE; + + /** + * This message is returned in the HTTP Response when an event is filtered (deliberately not validated). + */ + public static final String DEFAULT_MESSAGE_FOR_FILTERED_EVENTS = + "No validation results available. The action value may have caused the event to be filtered. Otherwise the event type or domain may be invalid."; + + /** + * Events are passed to the controller which will execute the validation(s). + */ + private ValidationController controller; + private AAIMicroServiceAuth aaiMicroServiceAuth; + + /** + * @param controller + */ + @Inject + public ValidateServiceImpl(final ValidationController controller, final AAIMicroServiceAuth aaiMicroServiceAuth) { + this.controller = controller; + this.aaiMicroServiceAuth = aaiMicroServiceAuth; + } + + @Override + public ResponseEntity<String> validate(@RequestHeader HttpHeaders headers, HttpServletRequest servletRequest, + @RequestBody String event) { + applicationLogger.startAudit(headers, servletRequest); + applicationLogger.info(ApplicationMsgs.MESSAGE_VALIDATION_REQUEST, headers + event); + + String path = (String) servletRequest.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + + if (applicationLogger.isDebugEnabled()) { + applicationLogger.debug(String.format( + "Received request. Path \"%s\", HttpHeaders \"%s\", ServletRequest \"%s\", Request Body \"%s\"", + path, headers, servletRequest.getMethod(), event)); + } + + // Additional name/value pairs according to EELF guidelines + applicationLogger.setContextValue("Protocol", "https"); + applicationLogger.setContextValue("Method", "POST"); + applicationLogger.setContextValue("Path", path); + applicationLogger.setContextValue("Query", servletRequest.getQueryString()); + + RequestHeaders requestHeaders = new RequestHeaders(headers); + String requestId = requestHeaders.getRequestId(); + if (requestId == null) { + requestId = UUID.randomUUID().toString(); + applicationLogger.info(ApplicationMsgs.MISSING_REQUEST_ID, requestId); + applicationLogger.setContextValue(MdcParameter.REQUEST_ID, requestId); + } + + ResponseEntity<String> response; + + try { + boolean authorized = + aaiMicroServiceAuth.validateRequest(servletRequest, servletRequest.getMethod(), "validate"); + + if (!authorized) { + response = ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body("User not authorized to perform the operation."); + } else { + response = validate(event); + } + } catch (Exception e) { + applicationLogger.error(ApplicationMsgs.PROCESS_REQUEST_ERROR, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + "Error while processing request. Please check the validation service logs for more details.\n"); + } + + StatusCode statusDescription; + int statusCode = response.getStatusCodeValue(); + if (Response.Status.Family.familyOf(statusCode).equals(Response.Status.Family.SUCCESSFUL)) { + statusDescription = StatusCode.COMPLETE; + } else { + statusDescription = StatusCode.ERROR; + } + applicationLogger.logAudit(statusDescription, Integer.toString(statusCode), + response.getStatusCode().getReasonPhrase(), response.getBody()); + + return response; + } + + /* + * (non-Javadoc) + * + * @see org.onap.aai.validation.services.ValidateService#validate(java.lang.String) + */ + @Override + public ResponseEntity<String> validate(String event) { + + try { + // Attempt to validate the event + Result result = controller.execute(event, "http"); + + if (result.validationSuccessful()) { + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON) + .body(result.getValidationResultAsJson()); + } else { + Optional<String> errorText = result.getErrorText(); + if (!errorText.isPresent() || errorText.get().isEmpty()) { + errorText = Optional.of(DEFAULT_MESSAGE_FOR_FILTERED_EVENTS); + } else { + applicationLogger.error(ApplicationMsgs.MALFORMED_REQUEST_ERROR, event); + } + return ResponseEntity.badRequest().body(errorText.orElse("")); + } + } catch (Exception e) { + // Unchecked runtime exception - this is intended to catch potential programming errors. + applicationLogger.error(ApplicationMsgs.PROCESS_REQUEST_ERROR, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + "Error while processing request. Please check the validation service logs for more details.\n" + e); + } + } + +} diff --git a/src/main/java/org/onap/aai/validation/servlet/StartupServlet.java b/src/main/java/org/onap/aai/validation/servlet/StartupServlet.java new file mode 100644 index 0000000..01fdc31 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/servlet/StartupServlet.java @@ -0,0 +1,95 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.servlet; + +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.onap.aai.cl.api.Logger; +import org.onap.aai.validation.config.TopicAdminConfig; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.services.EventPollingService; +import org.springframework.stereotype.Service; + +/** + * Main application service + * + */ +@Service +public class StartupServlet { + + private static final Logger applicationLogger = LogHelper.INSTANCE; + + private static final long DEFAULT_POLLING_INTERVAL = 10; + + private final EventPollingService eventPollingService; + private final TopicAdminConfig topicAdminConfig; + + /** + * @param eventPollingService + * @param topicAdminConfig + */ + @Inject + public StartupServlet(EventPollingService eventPollingService, TopicAdminConfig topicAdminConfig) { + this.eventPollingService = eventPollingService; + this.topicAdminConfig = topicAdminConfig; + } + + /** + * Called from Spring + */ + @PostConstruct + public void init() { + applicationLogger.info(ApplicationMsgs.STARTUP_SERVLET_INIT); + + // Schedule a polling service if consuming is enabled. + boolean consumingEnabled = topicAdminConfig.isConsumeEnable(); + if (consumingEnabled) { + Long consumerPollingIntervalSeconds = topicAdminConfig.getConsumePollingIntervalSeconds(); + + long interval; + if (consumerPollingIntervalSeconds == null) { + applicationLogger.info(ApplicationMsgs.POLLING_INTERVAL_CONFIG_NOT_PRESENT, + Long.toString(DEFAULT_POLLING_INTERVAL)); + interval = DEFAULT_POLLING_INTERVAL; + } else { + interval = consumerPollingIntervalSeconds; + } + + applicationLogger.info(ApplicationMsgs.POLLING_FOR_EVENTS, Long.toString(interval)); + Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(eventPollingService, 0, interval, + TimeUnit.SECONDS); + } else { + applicationLogger.info(ApplicationMsgs.POLLING_DISABLED); + } + + applicationLogger.info(ApplicationMsgs.STARTUP_SERVLET_INIT_SUCCESS); + } + + public EventPollingService getEventPollingService() { + return eventPollingService; + } + + public TopicAdminConfig getTopicAdminConfig() { + return topicAdminConfig; + } +} diff --git a/src/main/java/org/onap/aai/validation/util/GsonUtil.java b/src/main/java/org/onap/aai/validation/util/GsonUtil.java new file mode 100644 index 0000000..f6525c8 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/util/GsonUtil.java @@ -0,0 +1,124 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Optional; + +/** + * Static utility class + * + */ +public class GsonUtil { + + /** + * All methods are static. + */ + private GsonUtil() { + // Do not instantiate + } + + /** + * Tell Gson how to handle Optional fields. This factory builds a Type Adapter for a class wrapped with Optional + * + */ + public static class OptionalTypeAdapterFactory implements TypeAdapterFactory { + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { + Class<T> rawType = (Class<T>) type.getRawType(); + if (rawType != Optional.class) { + return null; + } + final ParameterizedType parameterizedType = (ParameterizedType) type.getType(); + final Type actualType = parameterizedType.getActualTypeArguments()[0]; + return new OptionalTypeAdapter(gson.getAdapter(TypeToken.get(actualType))); + } + } + + /** + * Implementation of the Optional Type Adapter + * + * @param <E> + */ + public static class OptionalTypeAdapter<E> extends TypeAdapter<Optional<E>> { + + private final TypeAdapter<E> adapter; + + public static final TypeAdapterFactory FACTORY = new OptionalTypeAdapterFactory(); + + /** + * @param adapter + */ + public OptionalTypeAdapter(TypeAdapter<E> adapter) { + this.adapter = adapter; + } + + @Override + public void write(JsonWriter out, Optional<E> value) throws IOException { + if (value != null && value.isPresent()) { // NOSONAR + adapter.write(out, value.get()); + } else { + boolean nullsAllowed = out.getSerializeNulls(); + if (nullsAllowed) { + out.setSerializeNulls(false); + } + out.nullValue(); + if (nullsAllowed) { + out.setSerializeNulls(true); + } + } + } + + @Override + public Optional<E> read(JsonReader in) throws IOException { + final JsonToken peek = in.peek(); + if (peek != JsonToken.NULL) { + return Optional.ofNullable(adapter.read(in)); + } + return Optional.empty(); + } + + } + + /** + * @return a new GsonBuilder with standard settings + */ + public static GsonBuilder createGsonBuilder() { + return new GsonBuilder().disableHtmlEscaping().serializeNulls().excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapterFactory(OptionalTypeAdapter.FACTORY); + } + + /** + * @return a new Gson instance + */ + public static Gson createGson() { + return createGsonBuilder().create(); + } + +} diff --git a/src/main/java/org/onap/aai/validation/util/JsonUtil.java b/src/main/java/org/onap/aai/validation/util/JsonUtil.java new file mode 100644 index 0000000..910eb63 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/util/JsonUtil.java @@ -0,0 +1,94 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.util; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import java.util.Map; + +/** + * Wrapper to hide the actual JSON API. + */ +public class JsonUtil { + + // General purpose. Excludes null values. + private static final Gson gsonDefault = new Gson(); + + // Only serialises fields with the Expose annotation + private static final Gson gsonForAnnotatedClasses = GsonUtil.createGson(); + + /** + * Purely a static class. + */ + private JsonUtil() { + // Deliberately empty + } + + /** + * Serialise the annotated object to a JSON string. + * + * @param obj + * the object to serialise + * @return the JSON representation of the object + */ + public static String toJson(Object obj) { + return gsonForAnnotatedClasses.toJson(obj); + } + + /** + * Deserialise the annotated object from a JSON string. + * + * @param <T> + * the type of the object + * @param json + * the JSON string + * @param classOfT + * the class of type T + * @return a new object instance + */ + public static <T> T toAnnotatedClassfromJson(String json, Class<T> classOfT) { + return gsonForAnnotatedClasses.fromJson(json, classOfT); + } + + /** + * Deserialise the object from a JSON string. + * + * @param <T> + * the type of the object + * @param json + * the JSON string + * @param classOfT + * the class of type T + * @return a new object instance + */ + public static <T> T fromJson(String json, Class<T> classOfT) { + return gsonDefault.fromJson(json, classOfT); + } + + /** + * To json element. + * + * @param map + * the map + * @return the json element + */ + public static JsonElement toJsonElement(Map<String, Object> map) { + return gsonForAnnotatedClasses.toJsonTree(map).getAsJsonObject(); + } + +} diff --git a/src/main/java/org/onap/aai/validation/util/StringUtils.java b/src/main/java/org/onap/aai/validation/util/StringUtils.java new file mode 100644 index 0000000..d50b5bf --- /dev/null +++ b/src/main/java/org/onap/aai/validation/util/StringUtils.java @@ -0,0 +1,100 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import org.onap.aai.cl.api.Logger; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; + +/** + * Utility class for String manipulation. + */ +public class StringUtils { + + private static final Logger applicationLogger = LogHelper.INSTANCE; + + /** + * All methods are static. + */ + private StringUtils() { + // Do not instantiate + } + + /** + * Utility method to strip a prefix or set of prefixes (identified by a delimiter sequence) from the string. This is + * achieved by finding the index of the last prefix delimiter in the string and removing all characters before and + * including this index. + * + * @param string the String to strip prefixes from + * @param prefixDelimiter the String that acts as the delimiter for the prefix(es) + * @return the String minus the prefixes + */ + public static String stripPrefix(String string, String prefixDelimiter) { + return string.contains(prefixDelimiter) + ? string.substring(string.lastIndexOf(prefixDelimiter) + prefixDelimiter.length()) : string; + } + + /** + * Strips a prefix identified by a delimiter. This is achieved by splitting the string in two around matches of the + * first occurrence of the given regular expression. + * + * @param string a String from which to strip a prefix + * @param regex the delimiting regular expression + * @return + * @throws ValidationServiceException If there is a problem with the provided regular expression. + */ + public static String stripPrefixRegex(String string, String regex) throws ValidationServiceException { + String[] strings = validParameters(string, regex) ? string.split(regex, 2) : new String[0]; + return strings.length == 2 ? strings[1] : string; + } + + /** + * Process a list of strings and strip the given suffix from each string in the list. + * + * @param stringList a list of strings + * @param suffix a suffix to be removed from the strings + * @return stripped list of strings. + */ + public static List<String> stripSuffix(List<String> stringList, String suffix) { + List<String> result = new ArrayList<>(); + for (String str : stringList) { + String stripped = str.endsWith(suffix) ? str.substring(0, str.indexOf(suffix)) : str; + result.add(stripped); + } + return result; + } + + private static boolean validParameters(String string, String regex) throws ValidationServiceException { + if (string != null && !string.isEmpty() && regex != null && !regex.isEmpty()) { + try { + Pattern.compile(regex); + return true; + } catch (PatternSyntaxException e) { + applicationLogger.error(ApplicationMsgs.STRING_UTILS_INVALID_REGEX, regex); + throw new ValidationServiceException(ValidationServiceError.STRING_UTILS_INVALID_REGEX, e, regex); + } + } + return false; + } +} |