From 95524c8ef8be0d41de8bb2b918f320e464ebb897 Mon Sep 17 00:00:00 2001 From: rb7147 Date: Wed, 18 Jul 2018 12:50:06 -0400 Subject: [PATCH] Decision BlackList Guard Enhancements While creating a decision Bl Guard Policy we are allowing to add Blacklist entries through file upload for bulk from GUI. Issue-ID: POLICY-901 Change-Id: I4031fd4a96937b9facc330cecf72777d701d4678 Signed-off-by: rb7147 --- .../pap/xacml/rest/components/DecisionPolicy.java | 16 +- .../rest/policycontroller/PolicyCreation.java | 12 +- .../resources/Decision_GuardBLPolicyTemplate.xml | 2 +- .../resources/Decision_GuardPolicyTemplate.xml | 2 +- .../policy/rest/adapter/PolicyRestAdapter.java | 21 + .../onap/policy/rest/adapter/ReturnBlackList.java | 51 ++ .../onap/policy/rest/util/PolicyValidation.java | 33 +- .../policy/rest/adapter/ReturnBlackListTest.java | 41 ++ POLICY-SDK-APP/pom.xml | 5 + .../ExportAndImportDecisionBlackListEntries.java | 380 +++++++++++++++ .../DecisionPolicyController.js | 121 ++++- .../PolicyTemplates/DecisionPolicyTemplate.html | 516 ++++++++++++--------- ...xportAndImportDecisionBlackListEntriesTest.java | 126 +++++ POLICY-SDK-APP/src/test/resources/BlackList.xls | Bin 0 -> 25088 bytes .../src/test/resources/DecisionPolicyData.txt | 1 + 15 files changed, 1083 insertions(+), 244 deletions(-) create mode 100644 ONAP-REST/src/main/java/org/onap/policy/rest/adapter/ReturnBlackList.java create mode 100644 ONAP-REST/src/test/java/org/onap/policy/rest/adapter/ReturnBlackListTest.java create mode 100644 POLICY-SDK-APP/src/main/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntries.java create mode 100644 POLICY-SDK-APP/src/test/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntriesTest.java create mode 100644 POLICY-SDK-APP/src/test/resources/BlackList.xls create mode 100644 POLICY-SDK-APP/src/test/resources/DecisionPolicyData.txt diff --git a/ONAP-PAP-REST/src/main/java/org/onap/policy/pap/xacml/rest/components/DecisionPolicy.java b/ONAP-PAP-REST/src/main/java/org/onap/policy/pap/xacml/rest/components/DecisionPolicy.java index 56c23ac2c..c8c540c34 100644 --- a/ONAP-PAP-REST/src/main/java/org/onap/policy/pap/xacml/rest/components/DecisionPolicy.java +++ b/ONAP-PAP-REST/src/main/java/org/onap/policy/pap/xacml/rest/components/DecisionPolicy.java @@ -167,7 +167,9 @@ public class DecisionPolicy extends Policy { if(policyAdapter.getRuleProvider().equals(GUARD_YAML) || policyAdapter.getRuleProvider().equals(GUARD_BL_YAML)){ Map yamlParams = new HashMap<>(); - yamlParams.put(DESCRIPTION, (policyAdapter.getPolicyDescription()!=null)? policyAdapter.getPolicyDescription(): "YAML Guard Policy"); + String blackListEntryType = policyAdapter.getBlackListEntryType() !=null ? policyAdapter.getBlackListEntryType(): "Use Manual Entry"; + String description = policyAdapter.getPolicyDescription() != null? policyAdapter.getPolicyDescription(): "YAML Guard Policy"; + yamlParams.put(DESCRIPTION, description + "@blEntry@" + blackListEntryType + "@blEntry@"); String fileName = policyAdapter.getNewFileName(); String name = fileName.substring(fileName.lastIndexOf('\\') + 1, fileName.length()); if ((name == null) || ("".equals(name))) { @@ -295,6 +297,16 @@ public class DecisionPolicy extends Policy { blackList.add(blackListString); } } + if(yamlParams.containsKey("appendBlackList")){ + String appendBlackListString = yamlParams.get("appendBlackList"); + List appendBlackList = null; + if(appendBlackListString!=null && !appendBlackListString.trim().isEmpty()){ + appendBlackList = Arrays.asList(appendBlackListString.split(",")); + for(int i=0; i - Denied! + Denied By Blacklist diff --git a/ONAP-PAP-REST/src/main/resources/Decision_GuardPolicyTemplate.xml b/ONAP-PAP-REST/src/main/resources/Decision_GuardPolicyTemplate.xml index 15465f3c0..809dc99fb 100644 --- a/ONAP-PAP-REST/src/main/resources/Decision_GuardPolicyTemplate.xml +++ b/ONAP-PAP-REST/src/main/resources/Decision_GuardPolicyTemplate.xml @@ -118,7 +118,7 @@ - Denied! + Denied By Guard diff --git a/ONAP-REST/src/main/java/org/onap/policy/rest/adapter/PolicyRestAdapter.java b/ONAP-REST/src/main/java/org/onap/policy/rest/adapter/PolicyRestAdapter.java index a9daf1732..e815fe2b9 100644 --- a/ONAP-REST/src/main/java/org/onap/policy/rest/adapter/PolicyRestAdapter.java +++ b/ONAP-REST/src/main/java/org/onap/policy/rest/adapter/PolicyRestAdapter.java @@ -127,6 +127,9 @@ public class PolicyRestAdapter { private String actionDictUrl = null; private String actionDictMethod = null; private YAMLParams yamlparams; + private List blackListEntries; + private List appendBlackListEntries; + private String blackListEntryType; //Rainy Day Decision private RainyDayParams rainyday; @@ -907,4 +910,22 @@ public class PolicyRestAdapter { public void setFaultDatas(ClosedLoopFaultTrapDatas faultDatas) { this.faultDatas = faultDatas; } + public List getAppendBlackListEntries() { + return appendBlackListEntries; + } + public void setAppendBlackListEntries(List appendBlackListEntries) { + this.appendBlackListEntries = appendBlackListEntries; + } + public List getBlackListEntries() { + return blackListEntries; + } + public void setBlackListEntries(List blackListEntries) { + this.blackListEntries = blackListEntries; + } + public String getBlackListEntryType() { + return blackListEntryType; + } + public void setBlackListEntryType(String blackListEntryType) { + this.blackListEntryType = blackListEntryType; + } } diff --git a/ONAP-REST/src/main/java/org/onap/policy/rest/adapter/ReturnBlackList.java b/ONAP-REST/src/main/java/org/onap/policy/rest/adapter/ReturnBlackList.java new file mode 100644 index 000000000..6ecf1b5e4 --- /dev/null +++ b/ONAP-REST/src/main/java/org/onap/policy/rest/adapter/ReturnBlackList.java @@ -0,0 +1,51 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP Policy Engine + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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.policy.rest.adapter; + +public class ReturnBlackList { + private boolean entryCheck; + private int actionValue; + private String entryValue; + + public boolean isEntryCheck() { + return entryCheck; + } + + public void setEntryCheck(boolean entryCheck) { + this.entryCheck = entryCheck; + } + + public int getActionValue() { + return actionValue; + } + + public void setActionValue(int actionValue) { + this.actionValue = actionValue; + } + + public String getEntryValue() { + return entryValue; + } + + public void setEntryValue(String entryValue) { + this.entryValue = entryValue; + } +} diff --git a/ONAP-REST/src/main/java/org/onap/policy/rest/util/PolicyValidation.java b/ONAP-REST/src/main/java/org/onap/policy/rest/util/PolicyValidation.java index 50985b12d..0478f5f95 100644 --- a/ONAP-REST/src/main/java/org/onap/policy/rest/util/PolicyValidation.java +++ b/ONAP-REST/src/main/java/org/onap/policy/rest/util/PolicyValidation.java @@ -940,20 +940,25 @@ public class PolicyValidation { responseString.append("Guard Params Time Units is Required" + HTML_ITALICS_LNBREAK); valid = false; } - }else if("GUARD_BL_YAML".equals(policyData.getRuleProvider())){ - if(policyData.getYamlparams().getBlackList()==null || policyData.getYamlparams().getBlackList().isEmpty()){ - responseString.append(" Guard Params BlackList is Required " + HTML_ITALICS_LNBREAK); - valid = false; - }else{ - for(String blackList: policyData.getYamlparams().getBlackList()){ - if(blackList==null || !(SUCCESS.equals(PolicyUtils.policySpecialCharValidator(blackList)))){ - responseString.append(" Guard Params BlackList Should be valid String" + HTML_ITALICS_LNBREAK); - valid = false; - break; - } - } - } - } + } else if ("GUARD_BL_YAML".equals(policyData.getRuleProvider()) + && "Use Manual Entry".equals(policyData.getBlackListEntryType())) { + if (policyData.getYamlparams().getBlackList() == null + || policyData.getYamlparams().getBlackList().isEmpty()) { + responseString + .append(" Guard Params BlackList is Required " + HTML_ITALICS_LNBREAK); + valid = false; + } else { + for (String blackList : policyData.getYamlparams().getBlackList()) { + if (blackList == null + || !(SUCCESS.equals(PolicyUtils.policySpecialCharValidator(blackList)))) { + responseString.append(" Guard Params BlackList Should be valid String" + + HTML_ITALICS_LNBREAK); + valid = false; + break; + } + } + } + } } } } diff --git a/ONAP-REST/src/test/java/org/onap/policy/rest/adapter/ReturnBlackListTest.java b/ONAP-REST/src/test/java/org/onap/policy/rest/adapter/ReturnBlackListTest.java new file mode 100644 index 000000000..e8f6684b1 --- /dev/null +++ b/ONAP-REST/src/test/java/org/onap/policy/rest/adapter/ReturnBlackListTest.java @@ -0,0 +1,41 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP Policy Engine + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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.policy.rest.adapter; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ReturnBlackListTest { + + @Test + public void testReturnBlackList() { + ReturnBlackList list = new ReturnBlackList(); + list.setActionValue(1); + list.setEntryCheck(true); + list.setEntryValue("Test"); + + assertTrue(1 == list.getActionValue()); + assertTrue(list.isEntryCheck()); + assertTrue("Test".equals(list.getEntryValue())); + } + +} diff --git a/POLICY-SDK-APP/pom.xml b/POLICY-SDK-APP/pom.xml index 7017017ce..1160a8f25 100644 --- a/POLICY-SDK-APP/pom.xml +++ b/POLICY-SDK-APP/pom.xml @@ -182,6 +182,11 @@ jackson-dataformat-xml ${jackson.version} + + com.google.code.gson + gson + 2.8.0 + org.elasticsearch diff --git a/POLICY-SDK-APP/src/main/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntries.java b/POLICY-SDK-APP/src/main/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntries.java new file mode 100644 index 000000000..8a37e9ddc --- /dev/null +++ b/POLICY-SDK-APP/src/main/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntries.java @@ -0,0 +1,380 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP Policy Engine + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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.policy.controller; + +import com.google.gson.Gson; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.json.JSONObject; +import org.onap.policy.common.logging.flexlogger.FlexLogger; +import org.onap.policy.common.logging.flexlogger.Logger; +import org.onap.policy.rest.adapter.PolicyRestAdapter; +import org.onap.policy.rest.adapter.ReturnBlackList; +import org.onap.policy.xacml.api.XACMLErrorConstants; +import org.onap.portalsdk.core.controller.RestrictedBaseController; +import org.onap.portalsdk.core.web.support.JsonMessage; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + + + +/** + * This class is used to import and export the black list entries which were used in the Decision Blacklist Guard YAML + * Policy. + * + */ +@Controller +@RequestMapping("/") +public class ExportAndImportDecisionBlackListEntries extends RestrictedBaseController { + + private static final Logger policyLogger = FlexLogger.getLogger(ExportAndImportDecisionBlackListEntries.class); + private static final String BLACKLISTENTRIESDATA = "blackListEntries"; + private static final String ACTION = "Action"; + private static final String BLACKLISTENTRY = "BlackListEntry"; + + /** + * This method is used to Export the Black List entries data from Decision BlackList Guard YAML Policy. So, user can + * update the file on adding or removing the entries, for updating the policies or using in other Environments. + * + * @param request the request contains the policy data. So, based on that we can populate and read and write the + * entries. + * @param response after reading and writing the blacklist list entries to file, the file is copied to tmp directory + * and making available to user to download from GUI. + * @throws IOException exception throws if anything goes wrong in the process. + */ + @RequestMapping(value = {"/policycreation/exportDecisionBlackListEntries"}, method = {RequestMethod.POST}) + public void exportBlackList(HttpServletRequest request, HttpServletResponse response) throws IOException { + try (HSSFWorkbook workBook = new HSSFWorkbook()) { + String requestData = request.getReader().lines().collect(Collectors.joining()); + JSONObject root = new JSONObject(requestData); + PolicyRestAdapter adapter = new Gson().fromJson(root.get("policyData").toString(), PolicyRestAdapter.class); + DecisionPolicyController controller = new DecisionPolicyController(); + controller.prePopulateDecisionPolicyData(adapter, null); + List blackLists = adapter.getYamlparams().getBlackList(); + HSSFSheet sheet = workBook.createSheet("BlackList"); + HSSFRow headingRow = sheet.createRow(0); + headingRow.createCell(0).setCellValue("Action"); + headingRow.createCell(1).setCellValue("BlackListEntry"); + + short rowNo = 1; + for (Object object : blackLists) { + HSSFRow row = sheet.createRow(rowNo); + row.createCell(0).setCellValue(1); + row.createCell(1).setCellValue(object.toString()); + rowNo++; + } + + String tmpFile = System.getProperty("catalina.base") + File.separator + "webapps" + File.separator + "temp"; + + /* + * Export FileName is the combination of BlacList+Scope+PolicyName+Version+PolicyCreatedDate. + * + */ + + SimpleDateFormat parseFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = parseFormat.parse(root.get("date").toString().replaceAll("\"", "")); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); + String formatedDate = dateFormat.format(date); + + String fileName = "BlackList_Scope_" + adapter.getDomainDir() + "_Name_" + adapter.getPolicyName() + + "_Version_" + root.get("version").toString() + "_Date_" + formatedDate + ".xls"; + + String deleteCheckPath = tmpFile + File.separator + fileName; + File deleteCheck = new File(deleteCheckPath); + if (deleteCheck.exists() && deleteCheck.delete()) { + policyLogger.info("Deleted the file from system before exporting a new file."); + } + + File temPath = new File(tmpFile); + if (!temPath.exists()) { + temPath.mkdir(); + } + + String file = temPath + File.separator + fileName; + File filepath = new File(file); + FileOutputStream fos = new FileOutputStream(filepath); + workBook.write(fos); + fos.flush(); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("application / json"); + request.setCharacterEncoding("UTF-8"); + + PrintWriter out = response.getWriter(); + String successMap = file.substring(file.lastIndexOf("webapps") + 8); + String responseString = new Gson().toJson(successMap); + JSONObject jsonResposne = new JSONObject("{data: " + responseString + "}"); + out.write(jsonResposne.toString()); + } catch (Exception e) { + policyLogger.error( + XACMLErrorConstants.ERROR_SYSTEM_ERROR + "Exception Occured while Exporting BlackList Entries" , e); + } + } + + /** + * This method is used to import the BlackList excel file into the system. Which is used to create Decision + * Blacklist Guard YAML Policy. + * + * @param request the HTTP request contains file upload stream form GUI. + * @param response the response is send to the GUI after reading the file input stream. + * @throws FileUploadException throws fileUpload Exception. + * @throws IOException throws IO Exceptions. + */ + @RequestMapping(value = {"/policycreation/importBlackListForDecisionPolicy"}, method = {RequestMethod.POST}) + public void importBlackListFile(HttpServletRequest request, HttpServletResponse response) throws Exception { + List items = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request); + List errorLogs = new ArrayList<>(); + Gson mapper = new Gson(); + errorLogs.add("error"); + Map model = new HashMap<>(); + if (items.isEmpty()) { + errorLogs.add("The File doesn't have any content and it is invalid."); + model.put(BLACKLISTENTRIESDATA, errorLogs); + } else { + readItems(items, errorLogs, model); + } + JsonMessage msg = new JsonMessage(mapper.toJson(model)); + JSONObject jsonResposne = new JSONObject(msg); + response.getWriter().write(jsonResposne.toString()); + } + + /** + * This method is used to read the first item, as we expect only one entry in the file upload. + * + * @param items The file entries which were uploaded from GUI. + * @param errorLogs on adding all incorrect entries, we can let user know what need to fixed. + * @param model Map which stores key value (blacklist and append list data) + * @throws Exception throws exception if it is not .xls format + */ + private void readItems(List items, List errorLogs, Map model) throws Exception { + Map files = new HashMap<>(); + + FileItem item = items.get(0); + files.put(item.getName(), item.getInputStream()); + File file = new File(item.getName()); + String fileName = file.getName(); + try (OutputStream outputStream = new FileOutputStream(file);) { + IOUtils.copy(item.getInputStream(), outputStream); + if (fileName.startsWith("BlackList") && fileName.endsWith(".xls")) { + readWorkBook(fileName, errorLogs, model); + } else { + errorLogs.add("The File Name should start with BlackList and must be .xls format."); + model.put(BLACKLISTENTRIESDATA, errorLogs); + } + } + Files.delete(file.toPath()); + } + + /** + * This method is used to read the workbook in xls file item. + * + * @param fileName fileName as input parameter + * @param errorLogs on adding all incorrect entries, we can let user know what need to fixed. + * @param model Map which stores key value (blacklist and append list data) + */ + private void readWorkBook(String fileName, List errorLogs, Map model) { + Set blackListEntries = new HashSet<>(); + Set appendBlackListEntries = new HashSet<>(); + try (Workbook workbook = WorkbookFactory.create(new File(fileName))) { + Sheet datatypeSheet = workbook.getSheetAt(0); + Iterator rowIterator = datatypeSheet.iterator(); + readExcelRows(rowIterator, blackListEntries, appendBlackListEntries, errorLogs); + if (errorLogs.size() == 1) { + model.put(BLACKLISTENTRIESDATA, blackListEntries); + model.put("appendBlackListEntries", appendBlackListEntries); + } else { + model.put(BLACKLISTENTRIESDATA, errorLogs); + } + } catch (Exception e) { + String error = "Error Occured While Reading File. Please check the format of the file."; + errorLogs.add(error); + model.put(BLACKLISTENTRIESDATA, errorLogs); + policyLogger.error(error , e); + } + } + + /** + * This method is used to read all the rows from imported Excel sheet and set to respective objects. + * + * @param rowIterator Excel Sheet rows are passed as input parameters. + * @param blackListEntries the data is set to this object, which is going to be added. + * @param appendBlackListEntries the data is set to this object which is going to be removed. + * @param errorLogs on adding all incorrect entries, we can let user know what need to fixed. + */ + private void readExcelRows(Iterator rowIterator, Set blackListEntries, + Set appendBlackListEntries, List errorLogs) { + while (rowIterator.hasNext()) { + Row currentRow = rowIterator.next(); + if (currentRow.getRowNum() == 0) { + continue; + } + Iterator cellIterator = currentRow.cellIterator(); + readExcelCells(cellIterator, blackListEntries, appendBlackListEntries, errorLogs); + } + } + + /** + * This method is used to read all the cells in the row. + * + * @param cellIterator iterating the cells and will parse based on the cell type. + * @param blackListEntries the data is set to this object, which is going to be added. + * @param appendBlackListEntries the data is set to this object which is going to be removed. + * @param errorLogs on adding all incorrect entries, we can let user know what need to fixed. + */ + private void readExcelCells(Iterator cellIterator, Set blackListEntries, + Set appendBlackListEntries, List errorLogs) { + boolean actionCheck = false; + boolean blackListCheck = false; + String blEntry = ""; + int actionEntry = 0; + int lineNo = 1; + while (cellIterator.hasNext()) { + Cell cell = cellIterator.next(); + if (ACTION.equalsIgnoreCase(getCellHeaderName(cell))) { + ReturnBlackList returnList = readActionCell(cell, lineNo, errorLogs); + actionEntry = returnList.getActionValue(); + actionCheck = returnList.isEntryCheck(); + } + if (BLACKLISTENTRY.equalsIgnoreCase(getCellHeaderName(cell))) { + ReturnBlackList returnList = readBlackListCell(cell, lineNo, errorLogs); + blEntry = returnList.getEntryValue(); + blackListCheck = returnList.isEntryCheck(); + actionEntry = returnList.getActionValue(); + } + lineNo++; + } + if (actionCheck && blackListCheck) { + addBlackListEntries(actionEntry, blackListEntries, appendBlackListEntries, blEntry); + } + } + + /** + * This method is used to read the Action cell entry. + * + * @param cell reading the action entry cell. + * @param lineNo counts the number of the cell. + * @param errorLogs on adding all incorrect entries, we can let user know what need to fixed. + * @return returns the response on setting to ReturnBlackList class. + */ + private ReturnBlackList readActionCell(Cell cell, int lineNo, List errorLogs) { + ReturnBlackList returnValues = new ReturnBlackList(); + String error = "Entry at row " + lineNo + " not added, the value in the " + ACTION + + "column is neither \"0\" nor \"1\""; + int actionEntry = 0; + try { + actionEntry = (int) cell.getNumericCellValue(); + returnValues.setEntryCheck(true); + if (actionEntry != 1 && actionEntry != 0) { + errorLogs.add(error); + } + } catch (Exception e) { + errorLogs.add(error); + policyLogger.error(error, e); + actionEntry = 0; + } + returnValues.setActionValue(actionEntry); + return returnValues; + } + + /** + * + * This method is used to read the BlackList cell entry. + * + * @param cell reading the blacklist entry cell. + * @param lineNo counts the number of the cell. + * @param errorLogs on adding all incorrect entries, we can let user know what need to fixed. + * @return returns the response on setting to ReturnBlackList class. + */ + private ReturnBlackList readBlackListCell(Cell cell, int lineNo, List errorLogs) { + ReturnBlackList returnValues = new ReturnBlackList(); + String blEntry = ""; + try { + blEntry = cell.getStringCellValue(); + returnValues.setEntryCheck(true); + } catch (Exception e) { + String error = "Entry at row " + lineNo + " not added, the value in the " + BLACKLISTENTRY + + " column is not a valid string"; + errorLogs.add(error); + policyLogger.error(error, e); + returnValues.setActionValue(0); + } + returnValues.setEntryValue(blEntry); + return returnValues; + } + + /** + * This method is used to add the data to blacklist and append list after parsing each and every row. + * + * @param actionEntry it has the input to add or not and holds either 0 or 1. + * @param blackListEntries list to add blacklist entries based on action entry = 1. + * @param appendBlackListEntries list to add append list entries based on action entry = 0. + * @param blEntry the value added to both entries based on action entry. + */ + private void addBlackListEntries(int actionEntry, Set blackListEntries, Set appendBlackListEntries, + String blEntry) { + if (actionEntry == 1) { + blackListEntries.add(blEntry); + } else { + appendBlackListEntries.add(blEntry); + } + } + + /** + * This method is used to identify the header of the cell. + * + * @param cell Excel sheet cell is passed as input parameter. + * @return the column header name value + */ + private String getCellHeaderName(Cell cell) { + return cell.getSheet().getRow(0).getCell(cell.getColumnIndex()).getRichStringCellValue().toString(); + } +} diff --git a/POLICY-SDK-APP/src/main/webapp/app/policyApp/policy-models/Editor/PolicyTemplateController/DecisionPolicyController.js b/POLICY-SDK-APP/src/main/webapp/app/policyApp/policy-models/Editor/PolicyTemplateController/DecisionPolicyController.js index 5b2bdb2b2..f560f4d58 100644 --- a/POLICY-SDK-APP/src/main/webapp/app/policyApp/policy-models/Editor/PolicyTemplateController/DecisionPolicyController.js +++ b/POLICY-SDK-APP/src/main/webapp/app/policyApp/policy-models/Editor/PolicyTemplateController/DecisionPolicyController.js @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * ONAP Policy Engine * ================================================================================ - * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,16 @@ * limitations under the License. * ============LICENSE_END========================================================= */ -angular.module('abs').controller('decisionPolicyController', ['$scope', 'PolicyAppService', 'policyNavigator', 'modalService', '$modal', 'Notification', function ($scope, PolicyAppService, PolicyNavigator, modalService, $modal, Notification) { +angular.module('abs').controller('decisionPolicyController', ['$scope', 'PolicyAppService', 'policyNavigator', 'modalService', '$modal', 'Notification', '$http', function ($scope, PolicyAppService, PolicyNavigator, modalService, $modal, Notification, $http) { $("#dialog").hide(); $scope.policyNavigator; $scope.savebutton = true; $scope.refreshCheck = false; + $scope.disableOnCreate = false; if(!$scope.temp.policy.editPolicy && !$scope.temp.policy.readOnly){ + $scope.disableOnCreate = true; $scope.temp.policy = { policyType : "Decision" } @@ -45,7 +47,11 @@ angular.module('abs').controller('decisionPolicyController', ['$scope', 'PolicyA if($scope.temp.policy.ruleProvider==undefined){ $scope.temp.policy.ruleProvider="Custom"; } - + + if($scope.temp.policy.blackListEntryType==undefined){ + $scope.temp.policy.blackListEntryType="Use Manual Entry"; + } + PolicyAppService.getData('getDictionary/get_OnapNameDataByName').then(function (data) { var j = data; $scope.data = JSON.parse(j.data); @@ -216,9 +222,15 @@ angular.module('abs').controller('decisionPolicyController', ['$scope', 'PolicyA $scope.temp.policy.ruleAlgorithmschoices = []; } }else if($scope.temp.policy.ruleProvider=="GUARD_BL_YAML"){ - if($scope.temp.policy.yamlparams.blackList.length==0){ - $scope.temp.policy.yamlparams.blackList = []; - } + if($scope.temp.policy.yamlparams.blackList == null || $scope.temp.policy.yamlparams.blackList.length==0){ + $scope.temp.policy.yamlparams.blackList = []; + } + if($scope.temp.policy.blackListEntries == null || $scope.temp.policy.blackListEntries.length==0){ + $scope.temp.policy.blackListEntries = []; + } + $scope.blackListEntries = []; + $scope.temp.policy.appendBlackListEntries = []; + $scope.blackListEntries = arrayUnique($scope.temp.policy.blackListEntries.concat($scope.temp.policy.yamlparams.blackList)); }else if($scope.temp.policy.ruleProvider=="GUARD_YAML"){ if($scope.temp.policy.yamlparams.targets.length==0){ $scope.temp.policy.yamlparams.targets = []; @@ -259,9 +271,11 @@ angular.module('abs').controller('decisionPolicyController', ['$scope', 'PolicyA $scope.addNewBL = function() { $scope.temp.policy.yamlparams.blackList.push(''); }; - $scope.removeBL = function() { - var lastItem = $scope.temp.policy.yamlparams.blackList.length-1; - $scope.temp.policy.yamlparams.blackList.splice(lastItem); + + $scope.removeBL = function(id) { + $scope.temp.policy.yamlparams.blackList = $scope.temp.policy.yamlparams.blackList.filter(function (obj){ + return obj !== id; + }); }; $scope.treatmentDatas = [{"treatmentValues" : $scope.temp.policy.rainyday.treatmentTableChoices}]; @@ -324,4 +338,93 @@ angular.module('abs').controller('decisionPolicyController', ['$scope', 'PolicyA $scope.temp.policy.attributes = []; } }; + + $scope.importButton = true; + var fd; + $scope.uploadBLFile = function(files) { + fd = new FormData(); + fd.append("file", files[0]); + var fileExtension = files[0].name.split(".")[1]; + if(fileExtension == "xls"){ + $scope.importButton = false; + $scope.$apply(); + }else{ + Notification.error("Upload the BlackList file which extends with .xls format."); + } + }; + + function arrayUnique(array) { + var a = array.concat(); + for(var i=0; i + ng-model="temp.policy.policyDescription" + title="Description field will accept any type of data." />
@@ -41,7 +41,8 @@ class="form-control" ng-disabled="temp.policy.readOnly" ng-model="temp.policy.onapName" ng-options="option for option in onapNameDictionaryDatas track by option" - required pattern="\S+" title="Select the dropdown value driven from OnapName (common)Dictionary."> + required pattern="\S+" + title="Select the dropdown value driven from OnapName (common)Dictionary.">
@@ -67,39 +68,40 @@
+ ng-disabled="temp.policy.readOnly" + ng-model="temp.policy.rainyday.serviceType" + placeholder="Service Type" title="Enter Service Type value." />
+ ng-disabled="temp.policy.readOnly" + ng-model="temp.policy.rainyday.vnfType" placeholder="VNF Type" + title="Enter VNF Type value." />
- +
- +
@@ -107,46 +109,44 @@
-
-
-
- -
-
- -
-
- -
-
- -
-
- +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
@@ -158,86 +158,145 @@
-
-
- -
-
- -
+
+
+
-
-
- -
-
- -
+
+
-
-
- -
-
- -
+
+
+
+
-
-
- -
-
- -
+
+
-
-
- -
-
- -
+
+
+
+
-
-
- - -
-
-
-
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ ng-disabled="temp.policy.readOnly" + ng-model="temp.policy.yamlparams.blackList[$index]" + placeholder="BlackList" />
-
-
+
- +
@@ -246,113 +305,122 @@
-
-
- -
-
- -
+
+
+
-
-
- -
-
- -
+
+
-
-
- -
-
- -
+
+
+
+
-
-
- - -
-
-
-
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ ng-disabled="temp.policy.readOnly" + ng-model="temp.policy.yamlparams.targets[$index]" + placeholder="Target" />
-
-
-
- -
-
- -
+
+
+
+
-
-
- -
-
- -
-
- +
+
+
+
+ +
+
+ +
+
+ -
+
-
-
- -
-
- -
+
+
+
+
-
-
- -
-
- -
+
+ +
+
+
+
+
+
+ +
+
@@ -364,7 +432,8 @@

@@ -375,18 +444,21 @@
+ placeholder="Attribute Value" + title="Enter the Attribute Value without any spaces and special characters" />
@@ -403,7 +475,8 @@
@@ -414,19 +487,22 @@
+ ng-model="settingschoice.value" placeholder="Settings Value" + title="Enter the Settings Attribute Value without any spaces and special characters" />
@@ -443,7 +519,8 @@
@@ -462,7 +539,8 @@ ng-disabled="temp.policy.readOnly" ng-model="ruleAlgorithmschoice.dynamicRuleAlgorithmField1" ng-options="option for option in attributeDictionaryDatas track by option" - name="dynamicRuleAlgorithmField1" title="Select the dropdown value driven from Attribute (common)Dictionary or Settings (Decision)Dictionary."> + name="dynamicRuleAlgorithmField1" + title="Select the dropdown value driven from Attribute (common)Dictionary or Settings (Decision)Dictionary.">
@@ -471,18 +549,21 @@ ng-disabled="temp.policy.readOnly" ng-model="ruleAlgorithmschoice.dynamicRuleAlgorithmCombo" ng-options="option for option in functionDefinitionDatas track by option" - name="dynamicRuleAlgorithmCombo" title="Select the dropdown value driven from FunctionDataType."> + name="dynamicRuleAlgorithmCombo" + title="Select the dropdown value driven from FunctionDataType.">
+ name="dynamicRuleAlgorithmField2" + title="Enter the Value without any spaces and special characters and for rule formation use A1, A2,..etc., based on above Rules." />
@@ -497,11 +578,14 @@
\ No newline at end of file diff --git a/POLICY-SDK-APP/src/test/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntriesTest.java b/POLICY-SDK-APP/src/test/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntriesTest.java new file mode 100644 index 000000000..bf01ac1d9 --- /dev/null +++ b/POLICY-SDK-APP/src/test/java/org/onap/policy/controller/ExportAndImportDecisionBlackListEntriesTest.java @@ -0,0 +1,126 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP Policy Engine + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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.policy.controller; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +public class ExportAndImportDecisionBlackListEntriesTest { + + private HttpServletRequest request; + private MockHttpServletResponse response; + String jsonString; + + @Before + public void setUp() throws Exception { + request = mock(HttpServletRequest.class); + response = new MockHttpServletResponse(); + } + + @Test + public void testExportBlackList() throws IOException{ + ClassLoader classLoader = getClass().getClassLoader(); + jsonString = IOUtils.toString(classLoader.getResourceAsStream("DecisionPolicyData.txt")); + try(BufferedReader reader = new BufferedReader(new StringReader(jsonString))){ + Mockito.when(request.getReader()).thenReturn(reader); + ExportAndImportDecisionBlackListEntries controller = new ExportAndImportDecisionBlackListEntries(); + controller.exportBlackList(request, response); + assertTrue("".equals(response.getContentAsString())); + }catch(Exception e){ + fail("Not expecting Exception while Exporting BlackListEntries."); + } + } + + @Test + public void testImportBlackList() throws Exception{ + MockHttpServletRequest request = new MockHttpServletRequest(); + ExportAndImportDecisionBlackListEntries controller = new ExportAndImportDecisionBlackListEntries(); + File file = new File("src/test/resources/BlackList.xls"); + try(FileInputStream targetStream = new FileInputStream(file)){ + ExportAndImportDecisionBlackListEntriesTest testController = Mockito.mock(ExportAndImportDecisionBlackListEntriesTest.class); + ServletInputStream inputStream = testController.getInputStream(getBytes(targetStream)); + Mockito.when(request.getInputStream()).thenReturn(inputStream); + String boundary = "===" + System.currentTimeMillis() + "==="; + request.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary); + request.addHeader("name", "BlackList.xls"); + controller.importBlackListFile(request, response); + assertTrue(response.getContentAsString().contains("data")); + }catch(Exception e){ + fail("Not expecting Exception while importing BlackListEntries."); + } + } + + public static byte[] getBytes(InputStream is) throws IOException { + int len; + int size = 1024; + byte[] buf; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + buf = new byte[size]; + while ((len = is.read(buf, 0, size)) != -1) + bos.write(buf, 0, len); + buf = bos.toByteArray(); + return buf; + } + + public ServletInputStream getInputStream(byte[] body) throws IOException { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); + ServletInputStream servletInputStream = new ServletInputStream() { + public int read() throws IOException { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + return servletInputStream; + } +} diff --git a/POLICY-SDK-APP/src/test/resources/BlackList.xls b/POLICY-SDK-APP/src/test/resources/BlackList.xls new file mode 100644 index 0000000000000000000000000000000000000000..228d7245fddf12f48075a0f72fdea66b6e05dd7f GIT binary patch literal 25088 zcmeHP2Urxzwyqh{kOTx#L}iGQvw)GLVjw6YxT}bSAt(ZuU`A2Q0TEqM#4KwT#GFM` z7KL3CBBHKw)iq$oJnx)tW|(PaM((@!e)oIto1U*aoUZ!MsdG-9uBxt{OMmFx-MF{o zJ;K=f5C!s`uS%3f=qxyQ5L{~$xXM#ZP1=$~!#1o@Jx-c>KxqQyMzFo8d`qak3AJwn zdlorFmAOe#oJdFV8|3HD$VFF&O^M}AQb_`dB#Gp|cee|<1+s7mB^8?lsjz%0i4sT@ zC7lzYbEy6>hu#lzu!t);jw+YRg+)|YE{Z?QaSLoK8v}LP zN2VN@!}W(bdi@{=i+IYYi#3S{Uvm>;XfkZeCH~-laU`Ckg9oCWS>Oq>ZA6Hg5Rd6A zT@*VJC2Y+mW+tY#rlyV&Ry{{q3NA)jbx|~inBPh4{>WfnOt^R^6QV*oH)pbU6vu&U zgz5^SnlN)Q(3-Nk6Kmp3NCb*v%@s)2bUDhBB8;-?*+ek0GXc3HEWILti#@S|9~2cW zXU!EYWX%;T;Yn;s4{>I64aE6TqKUBd^lWD8Mr^jtx~(era?>m6ZSgGo3}aSiPo=)+hF9C_U_NA}g+^^xu+R zEQP*63cWxIeT@|Q4k`3qQs~m;St%v|PAPP0_|o$4m4fr1mgfk?SEYr$PD-I4kU~Es zg}zS;ovyD+3%;~G&_!bS&u$7wRX|^%qm!?muTB(cyQm52v9YI&P8ktJS{@d}5Xuif zLI!FRw0CPMbXzHOD~WVb`DWG2?L*Ud1dR|n$+KAKj1$3=uI+u{F z4lU@6I0ijUS{|W(X#dd>(65m(aN1FT(^fzi%1QgDl7L>Qqoc2`4|=BCovwfm__UsB z{bQd+lAiSh`5AJy6VR_gH;eQU>H#X=LZ8z(dSn_pgmCLajC=0$&E$BKOffpvUV&&r zoL7J|jP>}CgvX0@8+anK6{#D%SVg!&KiRn4mLRCAR)C;}q=TSdTLFUMP&x=IyA>eV zhNOd_1zG`udXx@==4u59wod6FXxkrxRQb!c4s@$p10vfx(2QCGBHKC!tpky59cYNH zkwmt23|j{x+d9x6{jns@F0!oy{ngfh$hJM`)Z0ne{4n($f%v%Q{+d3Al1Cec=uB`)+Z5>M?2#yO1i88;% za|dtmD}LuUhsR}%hnE%PP@x`PnD=ugkqq+Y%^M+z3Ovv-^NblY_<|`Sjs%*j2o17R zo0?R1~?;nHk zJ!ONqv<8G3s+mf31rS<^D0PeKv~-1TkgeH2Jv}E#RwBP@bqiPc_xUH&C~iVkNNhBM z#=y^F359Cu9`0fwg<(?Mga52h1Y{D!DLnv039L`M5}#isRF9yQmX?`h&Y~pdo8#aH+2}CYIJabDB7+SbohUX+yx6Y~uS;grmdVDYB^wJFZ0Pt!u~Fv5 zzIgFMG8tYxsl2-l=E6<#d=O_gLedQ3LmTC%a1!3N`E6B`aM_V%+z$!yv& z*|@c2<1B*>9qA~gsq$j)H{6uWMjzR@%JwuPVB^{fPpk6ZfBvy#Htm^gBzT&s3^t;k zR^|Wk;%>=oIxyKt@U*Tn*ob;sm4AJGy<|29Og0ic%|-?rQBSM#=l``@GMkP}HWEC| zK?WO9Ppk4jQTtLd8$%`=37+O6gN>-CRr#O4yI(S!PRPbhwx@A{jaw@`&DkYqyA<1W zX0nmsX=XCmhwS;}A|>S@j{m6er}*%&d|NbodU8Eiy7&Dn)t z7b}?!m&rzgr#Z@CBkF0+E^E&GC7F#elZ^yVbCtnH)YF_@wqMGT#6|<&Z^1xN_}uhn zJU2V@Rir*E3F(`9mIj3=jSuFgXQc))Z3YF8VuBhoKt;WXkQbRwl0nUyLBW5Rpymuv zQJ*0M$57N$ohhk#Gbnfq6V!?UD(WFn<?|NB_`%^W-#Px&sPJv z;-NDbO~ygj&=<}U;U_AUME4O$g*|m;p;YvhKq~r5AQgSJ9OlYxvK07v9Im)1N(xYQ zUr@<-_(g&MgkWCVEM>@R$;hh=i<_GBa>+<|5`+uO*kvg~84E@kMLq}0WWb_}RI2YA zO(qLV*k`Fg342Bf6~0D`65_nt!ICO?HPCNTN@8SEWO{PyWNrv=QhFZ8PaKNfjjv7m zkx8%?2W_83**_TsnG8A)0dXdQKVUy!V0|=plY6N`-C!Kxmz)fa+EW~t)se4A{6J~R z3|GYo2LTo^6JWK0*WNJUG=LWwlaLh0g|PxdPz7z!>;Mo5eLn#NjROJc`3Zzmfd4u}Wfk`PD;0hVyCS&VCeFlPNQ@|I| z=*`U^CpZK~!jvH5F52MOYme?rnYUQAQ_AIuP@1H>gcC5T}3l^}xASIgPzq&p14Nl->QW#EYnrtJVah^lf+6N^T842WVTMnbbB z^p!vo`br=PeTDI{N0t((%1m4YB_+`N5U6viaGXyoOO5nI&kRXOPXui_iI-B)0&9eT zGo*u^P+u*bn}yEZi-Jf_M}sq_8WapD2vj(2Y(>h4#HJHa?}D&F*H&8l7#95Yu&ASAd&ns$Q^w+uGPuw!WRtIC5IVXbc}HXC1{M?u8TU&{Pn`?_ zAf1<%?&I$yJaZPFxd_i(`Oq2RkVKgH2LqF6jy@!mhTim>j%>1$qpG=5cugCI;$e5V$-zKzpXaYue?#Zb@-P?|0^Lr+qn9PwskIz>v}#&Jz=DBpnO%~ z)xyf%#Iy;MHXqTdUy2XaZC4kSpydo1Q$*HD<#gqw0BN! zs!{HiE1x~s7rRZc$oiJmraEKjtq#u?Ec19!>0A3q;b2MU5xrkN`n1aIe(wIGo^FA= z2KjQb_npqW_I%r?{+0SZ_7{$HeQ&?TX2HhlpU#AQGAMa6n%C`ew{3U0Z`2pJEichn z5b;MG*TC+tQ$5!$cwT?~r^Ms8b4qijxs9$0EaU5qdGGl|_iELQkuyLeeCWK2>#OMc z*2kwoPi|mWTvrm2%1cahu*d!ErGJbqchs2IFy}<}<;uH5L1inK1^myKQo`c9f0J>>$yj1iMjj8~(a;~=5z08dh zbL-rrk6HGMD!sVp*azZU*wMRU!DUd@`pnm^eVs%S3cRol-$^lF;z zR>N&K2Xs1Tn|QCH-Cs{9b*S9GMEB2uzoiY$G_A>JbPFCBzV2`Pp~DOII?f%l{zGX=uI3TN)QiTph6A`3`Cpxs z*O-0rd%pAU(22ir z*;-Ry+&O){*xTyl-t6SiquCo0p4M)dpZ73bC0y0;{rA`d-D7@#vQhU=o9v z(bk-tzTN6C+ht#(?D_{87_q!qR(WQxA5K~DI9_e&Ex#3oDfc@s>!&#LP+wJD$5Gpd z_y48G*Rb_oo(W4gDJ<4VIH$g9{#xU66}rO$SC&5bQ)%}}*5ogr->=E{JTdvx+cjau zcaDDzb2}8utL(ffCi23!*SG)Vf2(ePr@?djmx_G;fUS4VtLyS#uAh9&o&ce~>89aH^scIU6JRZkV3HS;-}=cV&x z!nQ6Keq-N>F#Y??`;zfzx^^mEtUf93{k8i&ay|}fTT#`x=?1%tOaG~hdupZxRr5wn zDjlT%vhve~v%P(uRCSwZWWF+c-cIu^VfTaUmX`E;Yus-^U-iN zdX`;te$aV;p5=t?g&S5CD0})ir!MPM{OQ_R%{hue4vxG#DRbwfRQ9@CGP?X}Be(qU z?dkrtdzU=SD?FOs#rVAc!>qL*O!XYXZ#WG!(fzXBV~%#eJH8g-ucBicwBNUx?o~N@ zqNnkV)W&(bWzhrnSQMFfU30g2`RY;Us$CyD7eDP38MgB2n;RGQga;O0zC6-y*QGvZ zR&7diEx$4K>-KE#rFM2}+HYy-pF6zGUYnAaCOt!Irp{8@KX6BJUBEKGz9Wo#o>xCO z?~m70Y+mmg+Sc@B(A&&cM-$!}hjfp#&h*;4)#6p3nIE+7pG#Pw!cOl{b|UOqIp?PX zgHNbWJih11qf?8XJKe|+vg_FXMm{&&!A@bp?G=-Y$6L(*Syzwc;(ck%;VX9YN4#|C z8xmd$x@}i;-mUOk|LN@1rPKbJeP!>cVbjdsZC{!d;kr*p!@7Istp#sQPxBO)?x-qL z)K4l~w(_;d^7T3fOHB9A-n?-~@e13%zfLheZmydWwPoJgqwUu1xgYH|IHxit?aem&c16=4?C7lTW!I?o_2GxR^QKg2DQL5dJr-wwnD*<}6-TE$2!Bzv^y$;g z^ZQN6&(8xC_0}cX?3tRsH8g+jO{HGrmpPuPw%BO5*|3*Rnbtkq9yL$;#gDpP`{GQI zes7*yZ=>0v-ka;D7%kfQx#PowX$k>V{@%I`#~--=Z9Kb5&#ve9PJxT>IgR#t&UwF6 z{j$=iWB%qr?q7#HU(8eQ?qxDabIs$pB%cS ze17D?SjTOVdAqdN*@n#Qwq)$^tH;M~>GUD=wwAipMT3F+wt?!04!^qg+%5w*-k6Ik z!))Ft?_9FLc~n|c7M?>ZrNbV01LBVOM9nPscpIrT3&c*VaTVfJsz8vhCVde{HynG$1V+M%*fpE zdJSvZIkOW{+EZ(Y!O9^8{C(d%IxdKFFwzhIKGZv{ZjO@Wrt?*IP1o%3SG+Lt=+U!( z3aidy+rf)*Y5j1>CbCKBl@A##4#4b~HkWn;eTsBbnFX00z2<@j&;xGKa}rTM6N%aD zYDT&)35n{1F)^4<(2TJN=3hmnk>L_U1}ouM3kJ+eBphl#7klbt4GiDbhzy3{23ZWW z#UeD!2Yd+qIiR-K=YZ;LJ-khAF!Cqnu%g2bKA+Kq(LK(FuwksLMV;%wp1^D-hkV`s z`bt355YGsTZcWi`sQoO8l0^-}XiRgMgMfBHX2P8^7@`r<4pKIidp5PV5axd3iQg2_ z+Y?Vq;rT01E0Ob!@K%NeQsDrV1qik*TdXu}cRdA1_?QspcMAnzG+AtqQqkLRz<`V} zgNPgI0B1H}aLmR<-FS{I8#a~}MMGvx8s=xxa4`pyhFnEy-zd8f&@gWF6$`GA$8?xV z!6V*#<%##OzGw^j9v++Tp|!N|eFaqxkIk4kqo#$EVAMT4HkZQzz7|#MT9|iv1^0%) z$cX;+frT9hDWCHKI&N5Y5qQ@kG0n2!Ycb%BH*uOmk;)dwX;0!rvVnm37EGZp?Tb`i z`R+fff$!kC%G9)wI3IMYyK~mR`4kkdy>l6d*mT`jhbt9hAfXlUz9l3yAJXyLks*+9 zE@c!XbiId=5!XxOigqa<4-+rAN514tOsTlaLYI_pat+8eAlHCg19A<>H6Yi3Tmy0q z$Tc9>fLsG|4aha{@7I8|@js5=*)`QQh4zNp%kyFUZ~JKv%*zlZ2JIuz_CvyX0GtyT z00}>X$GHHUeH;Nv8`2m^I5!Xv3ExboK*BkLNsw?YM-C*xoB=%#piPwrveaW207jlT zaVcC+LNtJiNI3I=5`-nEj!TQ@@zT?%xi2gL8!np@%r|i19Le~l8kY2Ky2f*q5fP%2 zlgIgGB*X}XM0iU`j?U5Dx;eQx+qt_tI@-CqxwzOxxjDw#b$4@jjp`QT5$WWFb0+dl zt^v6Q