diff --git a/avni-server-api/src/main/java/org/avni/server/service/MetadataDiffService.java b/avni-server-api/src/main/java/org/avni/server/service/MetadataDiffService.java new file mode 100644 index 000000000..9d8fb0aa0 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/service/MetadataDiffService.java @@ -0,0 +1,370 @@ +package org.avni.server.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.io.*; +import java.nio.file.Files; +import java.util.*; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +@Service +public class MetadataDiffService { + + private static final Logger logger = LoggerFactory.getLogger(MetadataDiffService.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public Map compareMetadataZips(MultipartFile zipFile1, MultipartFile zipFile2) throws IOException { + Map result = new HashMap<>(); + File tempDir1 = null, tempDir2 = null; + + try { + tempDir1 = extractZip(zipFile1); + tempDir2 = extractZip(zipFile2); + + List files1 = listJsonFiles(tempDir1); + List files2 = listJsonFiles(tempDir2); + + Map> jsonMap1 = parseJsonFiles(files1, tempDir1); + Map> jsonMap2 = parseJsonFiles(files2, tempDir2); + + Set fileNames1 = jsonMap1.keySet(); + Set fileNames2 = jsonMap2.keySet(); + + Set commonFileNames = new HashSet<>(fileNames1); + commonFileNames.retainAll(fileNames2); + + for (String fileName : commonFileNames) { + Map jsonMapFile1 = jsonMap1.get(fileName); + Map jsonMapFile2 = jsonMap2.get(fileName); + + if (jsonMapFile1 != null && jsonMapFile2 != null) { + Map fileDifferences = findDifferences(jsonMapFile1, jsonMapFile2); + if (!fileDifferences.isEmpty()) { + result.put(fileName, fileDifferences); + } + } + } + + Set missingInZip1 = findMissingFiles(fileNames1, fileNames2); + if (!missingInZip1.isEmpty()) { + result.putAll(missingFilesMap(missingInZip1, "Missing Files in UAT ZIP")); + } + + Set missingInZip2 = findMissingFiles(fileNames2, fileNames1); + if (!missingInZip2.isEmpty()) { + result.putAll(missingFilesMap(missingInZip2, "Missing Files in PROD ZIP")); + } + + } catch (IOException e) { + logger.error("Error comparing metadata ZIPs: " + e.getMessage(), e); + Map errorResult = new HashMap<>(); + errorResult.put("error", "Error comparing metadata ZIPs: " + e.getMessage()); + result.put("error", errorResult); + } finally { + if (tempDir1 != null) { + deleteDirectory(tempDir1); + } + if (tempDir2 != null) { + deleteDirectory(tempDir2); + } + } + return result; + } + + private File extractZip(MultipartFile zipFile) throws IOException { + File tempDir = Files.createTempDirectory("metadata-zip").toFile(); + + try (ZipInputStream zipInputStream = new ZipInputStream(zipFile.getInputStream())) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (!entry.isDirectory()) { + File file = new File(tempDir, entry.getName()); + File parentDir = file.getParentFile(); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + + try (OutputStream outputStream = new FileOutputStream(file)) { + byte[] buffer = new byte[1024]; + int length; + while ((length = zipInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + } + } + zipInputStream.closeEntry(); + } + } + return tempDir; + } + + private List listJsonFiles(File directory) { + List jsonFiles = new ArrayList<>(); + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + jsonFiles.addAll(listJsonFiles(file)); + } else if (file.isFile() && file.getName().toLowerCase().endsWith(".json")) { + jsonFiles.add(file); + } + } + } + return jsonFiles; + } + + private Map> parseJsonFiles(List files, File rootDir) throws IOException { + Map> jsonMap = new HashMap<>(); + + for (File file : files) { + String relativePath = getRelativePath(file, rootDir); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + StringBuilder jsonContent = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + jsonContent.append(line); + } + + Map jsonMapFile = new HashMap<>(); + if (jsonContent.toString().trim().startsWith("[")) { + List> jsonArray = objectMapper.readValue(jsonContent.toString(), new TypeReference>>() {}); + for (Map jsonObject : jsonArray) { + String uuid = (String) jsonObject.get("uuid"); + if (uuid != null) { + jsonObject.remove("filename"); + jsonMapFile.put(uuid, jsonObject); + } + } + } else { + Map jsonObject = objectMapper.readValue(jsonContent.toString(), new TypeReference>() {}); + String uuid = (String) jsonObject.get("uuid"); + if (uuid != null) { + jsonObject.remove("filename"); + jsonMapFile.put(uuid, jsonObject); + } + } + jsonMap.put(relativePath, jsonMapFile); + } + } + return jsonMap; + } + + private String getRelativePath(File file, File rootDir) { + String filePath = file.getPath(); + String rootPath = rootDir.getPath(); + return filePath.substring(rootPath.length() + 1); + } + + private Map findDifferences(Map jsonMap1, Map jsonMap2) { + Map differences = new HashMap<>(); + boolean hasDifferences = false; + String uuid = "null"; + for (Map.Entry entry : jsonMap1.entrySet()) { + uuid = entry.getKey(); + Object json1 = entry.getValue(); + Object json2 = jsonMap2.get(uuid); + if (json2 != null) { + Map diff = findJsonDifferences(castToStringObjectMap(json1), castToStringObjectMap(json2)); + if (!diff.isEmpty()) { + differences.put(uuid, diff); + hasDifferences = true; + } + } else { + differences.put(uuid, createFieldDiff(null, json1, "removed")); + hasDifferences = true; + } + } + + for (Map.Entry entry : jsonMap2.entrySet()) { + String uuid2 = entry.getKey(); + if (!jsonMap1.containsKey(uuid2)) { + differences.put(uuid2, createFieldDiff(null, entry.getValue(), "added")); + hasDifferences = true; + } + } + + if (!hasDifferences) { + differences.put(uuid, createFieldDiff(null, null, "noModification")); + } + return differences; + } + + private Map findJsonDifferences(Map json1, Map json2) { + Map differences = new LinkedHashMap<>(); + if (json1 == null && json2 == null) { + return differences; + } + + if (json1 == null) { + json2.forEach((key, value) -> differences.put(key, createFieldDiff(null, value, "added"))); + return differences; + } + + if (json2 == null) { + json1.forEach((key, value) -> differences.put(key, createFieldDiff(value, null, "removed"))); + return differences; + } + + for (Map.Entry entry : json1.entrySet()) { + String key = entry.getKey(); + Object value1 = entry.getValue(); + Object value2 = json2.get(key); + + if (key.equals("id")) { + continue; + } + if (value2 == null) { + differences.put(key, createFieldDiff(value1, null, "removed")); + } else { + if (value1 instanceof Map && value2 instanceof Map) { + Map subDiff = findJsonDifferences((Map) value1, (Map) value2); + if (!subDiff.isEmpty()) { + differences.put(key, createObjectDiff((Map) value1, (Map) value2, "modified")); + } + } else if (value1 instanceof List && value2 instanceof List) { + List> listDiff = findArrayDifferences((List) value1, (List) value2); + if (!listDiff.isEmpty()) { + differences.put(key, createArrayDiff((List) value1, (List) value2, "modified")); + } + } else if (!value1.equals(value2)) { + differences.put(key, createFieldDiff(value1, value2, "modified")); + } + } + } + + for (Map.Entry entry : json2.entrySet()) { + String key = entry.getKey(); + if (!json1.containsKey(key)) { + differences.put(key, createFieldDiff(null, entry.getValue(), "added")); + } + } + + return differences; + } + + private List> findArrayDifferences(List array1, List array2) { + List> differences = new ArrayList<>(); + int maxSize = Math.max(array1.size(), array2.size()); + + for (int i = 0; i < maxSize; i++) { + if (i >= array1.size()) { + differences.add(createFieldDiff(null, array2.get(i), "added")); + } else if (i >= array2.size()) { + differences.add(createFieldDiff(array1.get(i), null, "removed")); + } else { + Object value1 = array1.get(i); + Object value2 = array2.get(i); + + if (value1 instanceof Map && value2 instanceof Map) { + Map subDiff = findJsonDifferences(castToStringObjectMap(value1), castToStringObjectMap(value2)); + if (!subDiff.isEmpty()) { + differences.add(createObjectDiff(castToStringObjectMap(value1), castToStringObjectMap(value2), "modified")); + } + } else if (!value1.equals(value2)) { + differences.add(createFieldDiff(value1, value2, "modified")); + } + } + } + return differences; + } + + private Map createFieldDiff(Object oldValue, Object newValue, String changeType) { + Map fieldDiff = new LinkedHashMap<>(); + + if(!"noModification".equals(changeType)) { + if (oldValue == null && newValue != null) { + fieldDiff.put("dataType", getDataType(newValue)); + } else if (oldValue != null && newValue == null) { + fieldDiff.put("dataType", getDataType(oldValue)); + } else if (oldValue != null && newValue != null) { + fieldDiff.put("dataType", getDataType(newValue)); + } else { + fieldDiff.put("dataType", "object"); + } + } + fieldDiff.put("changeType", changeType); + if (oldValue != null) { + fieldDiff.put("oldValue", oldValue); + } + if (newValue != null) { + fieldDiff.put("newValue", newValue); + } + return fieldDiff; + } + + private Map createObjectDiff(Map oldValue, Map newValue, String changeType) { + Map objectDiff = new LinkedHashMap<>(); + Map fieldsDiff = findDifferences(oldValue, newValue); + + if (!fieldsDiff.isEmpty() && !"noModification".equals(changeType)) { + objectDiff.put("dataType", "object"); + objectDiff.put("changeType", changeType); + objectDiff.put("fields", fieldsDiff); + } + return objectDiff; + } + + private Map createArrayDiff(List oldValue, List newValue, String changeType) { + Map arrayDiff = new LinkedHashMap<>(); + + List> itemsDiff = findArrayDifferences(oldValue, newValue); + if (!itemsDiff.isEmpty() && !"noModification".equals(changeType)) { + arrayDiff.put("dataType", "array"); + arrayDiff.put("changeType", changeType); + arrayDiff.put("items", itemsDiff); + } + return arrayDiff; + } + + private Set findMissingFiles(Set fileNames1, Set fileNames2) { + Set missingFiles = new HashSet<>(fileNames1); + missingFiles.removeAll(fileNames2); + return missingFiles; + } + + private Map missingFilesMap(Set missingFiles, String message) { + Map missingFilesMap = new LinkedHashMap<>(); + missingFilesMap.put("message", message); + missingFilesMap.put("files", missingFiles); + return missingFilesMap; + } + + private void deleteDirectory(File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + directory.delete(); + } + + private String getDataType(Object value) { + if (value instanceof Map) { + return "object"; + } else if (value instanceof List) { + return "array"; + } else { + return "primitive"; + } + } + + @SuppressWarnings("unchecked") + private Map castToStringObjectMap(Object obj) { + if (obj instanceof Map) { + return (Map) obj; + } + return new HashMap<>(); + } +} diff --git a/avni-server-api/src/main/java/org/avni/server/web/MetadataDiffController.java b/avni-server-api/src/main/java/org/avni/server/web/MetadataDiffController.java new file mode 100644 index 000000000..ce4199669 --- /dev/null +++ b/avni-server-api/src/main/java/org/avni/server/web/MetadataDiffController.java @@ -0,0 +1,32 @@ +package org.avni.server.web; + +import org.avni.server.service.MetadataDiffService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Map; + +@RestController +@RequestMapping("/api") +public class MetadataDiffController { + private final MetadataDiffService metadatadiffService; + + @Autowired + public MetadataDiffController(MetadataDiffService metadatadiffService) { + this.metadatadiffService = metadatadiffService; + } + + @PostMapping("/compare-metadata") + @PreAuthorize("hasAnyAuthority('user')") + public ResponseEntity compareMetadataZips(@RequestParam("file1") MultipartFile file1, + @RequestParam("file2") MultipartFile file2) throws IOException { + + Map result = metadatadiffService.compareMetadataZips(file1, file2); + + return ResponseEntity.ok(result); + } +} diff --git a/avni-server-api/src/test/java/org/avni/server/web/MetadataDiffControllerIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/web/MetadataDiffControllerIntegrationTest.java new file mode 100644 index 000000000..71eb2caa8 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/web/MetadataDiffControllerIntegrationTest.java @@ -0,0 +1,25 @@ +package org.avni.server.web; + +import org.avni.server.common.AbstractControllerIntegrationTest; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +public class MetadataDiffControllerIntegrationTest extends AbstractControllerIntegrationTest { + + @Test + public void testCompareMetadataZips() throws Exception { + setUser("demo-user"); + MockMultipartFile file1 = new MockMultipartFile("file1", "file1.zip", MediaType.MULTIPART_FORM_DATA_VALUE, "zip file content".getBytes()); + MockMultipartFile file2 = new MockMultipartFile("file2", "file2.zip", MediaType.MULTIPART_FORM_DATA_VALUE, "zip file content".getBytes()); + + mockMvc.perform(MockMvcRequestBuilders.multipart("/api/compare-metadata") + .file(file1) + .file(file2)) + .andExpect(status().isOk()); + } +} \ No newline at end of file