/* * Copyright (c) 2015 EMC Corporation * All Rights Reserved */ package com.emc.apidocs.differencing; import com.emc.apidocs.model.*; import com.google.common.collect.Lists; import java.util.Collections; import java.util.Comparator; import java.util.List; public class DifferenceEngine { private static List<ApiService> oldApi; private static List<ApiService> newApi; public static synchronized ApiDifferences calculateDifferences(List<ApiService> oldServices, List<ApiService> newServices) { oldApi = oldServices; newApi = newServices; ApiDifferences apiDifferences = new ApiDifferences(); apiDifferences.newServices = findNewServices(); apiDifferences.removedServices = findRemovedServices(); apiDifferences.modifiedServices = findModifiedServices(); return apiDifferences; } private static List<ApiService> findNewServices() { List<ApiService> newServices = Lists.newArrayList(); for (ApiService apiService : newApi) { if (!containsService(apiService.getFqJavaClassName(), oldApi)) { newServices.add(apiService); } } return newServices; } private static List<ApiService> findRemovedServices() { List<ApiService> removedServices = Lists.newArrayList(); for (ApiService apiService : oldApi) { if (!containsService(apiService.getFqJavaClassName(), newApi)) { removedServices.add(apiService); } } return removedServices; } private static List<ApiServiceChanges> findModifiedServices() { List<ApiServiceChanges> changedServices = Lists.newArrayList(); for (ApiService newService : newApi) { ApiService oldService = findService(newService.getFqJavaClassName(), oldApi); if (oldService != null) { ApiServiceChanges serviceChanges = new ApiServiceChanges(); serviceChanges.service = newService; serviceChanges.newMethods = findNewMethods(newService, oldService); serviceChanges.removedMethods = findRemovedMethods(oldService, newService); serviceChanges.modifiedMethods = findModifiedMethods(oldService, newService); if (serviceChanges.containsChanges()) { changedServices.add(serviceChanges); } } } // Sort Alphabetically Collections.sort(changedServices, new Comparator<ApiServiceChanges>() { @Override public int compare(ApiServiceChanges o1, ApiServiceChanges o2) { return o1.service.getTitle().compareTo(o2.service.getTitle()); } }); return changedServices; } private static List<ApiMethod> findDeprecatedMethods(ApiService newApiService) { List<ApiMethod> deprecatedMethods = Lists.newArrayList(); for (ApiMethod apiMethod : newApiService.methods) { if (apiMethod.isDeprecated) { deprecatedMethods.add(apiMethod); } } return deprecatedMethods; } private static List<ApiMethod> findNewMethods(ApiService newApiService, ApiService oldApiService) { List<ApiMethod> newMethods = Lists.newArrayList(); for (ApiMethod newApiMethod : newApiService.methods) { if (!containsMethod(newApiMethod.javaMethodName, oldApiService.methods)) { newMethods.add(newApiMethod); } } return newMethods; } private static List<ApiMethod> findRemovedMethods(ApiService oldApiService, ApiService newApiService) { List<ApiMethod> removedMethods = Lists.newArrayList(); for (ApiMethod oldApiMethod : oldApiService.methods) { if (!containsMethod(oldApiMethod.javaMethodName, newApiService.methods)) { removedMethods.add(oldApiMethod); } } return removedMethods; } private static List<ApiMethodChanges> findModifiedMethods(ApiService oldService, ApiService newService) { List<ApiMethodChanges> methodChanges = Lists.newArrayList(); for (ApiMethod newMethod : newService.methods) { ApiMethod oldMethod = findMethod(newMethod.javaMethodName, oldService.methods); if (oldMethod != null) { ApiMethodChanges apiMethodChanges = new ApiMethodChanges(); apiMethodChanges.method = newMethod; apiMethodChanges.newRoles = findAddedValues(oldMethod.roles, newMethod.roles); apiMethodChanges.removedRoles = findAddedValues(oldMethod.roles, newMethod.roles); apiMethodChanges.headerParameters = generateMergedFields(oldMethod.headerParameters, newMethod.headerParameters); apiMethodChanges.requestHeadersChanged = containsChanges(apiMethodChanges.headerParameters); apiMethodChanges.pathParameters = generateMergedFields(oldMethod.pathParameters, newMethod.pathParameters); apiMethodChanges.pathParametersChanged = containsChanges(apiMethodChanges.pathParameters); apiMethodChanges.queryParameters = generateMergedFields(oldMethod.queryParameters, newMethod.queryParameters); apiMethodChanges.queryParametersChanged = containsChanges(apiMethodChanges.queryParameters); apiMethodChanges.responseHeaders = generateMergedFields(oldMethod.responseHeaders, newMethod.responseHeaders); apiMethodChanges.responseHeadersChanged = containsChanges(apiMethodChanges.responseHeaders); // Get Request Payload Changes if (oldMethod.input != null && newMethod.input != null) { apiMethodChanges.input = generateMergedClass(oldMethod.input, newMethod.input); apiMethodChanges.requestPayloadChanged = containsChanges(apiMethodChanges.input); } // Get Response Payload Changes if (oldMethod.output != null && newMethod.output != null) { apiMethodChanges.output = generateMergedClass(oldMethod.output, newMethod.output); apiMethodChanges.responsePayloadChanged = containsChanges(apiMethodChanges.output); } if (apiMethodChanges.containsChanges()) { methodChanges.add(apiMethodChanges); } } } return methodChanges; } // Creates a new Class which is a mix of the old and new class, fields are marked with change markers public static ApiClass compareClasses(ApiClass oldClass, ApiClass newClass) { List<ApiField> oldFields = oldClass.fields; List<ApiField> newFields = newClass.fields; ApiClass diffClass = new ApiClass(); int oldClassPos = 0; for (int newClassPos = 0; newClassPos < newClass.fields.size(); newClassPos++) { ApiField newClassField = newFields.get(newClassPos); ApiField oldClassField = oldFields.get(oldClassPos); ChangeState fieldChangeState = ChangeState.NOT_CHANGED; if (oldClassField.name.equals(newClassField.name)) { fieldChangeState = ChangeState.NOT_CHANGED; oldClassPos++; } else { // Search forward in the new Class to see if this field appears (which means fields were deleted) int foundPosition = -1; for (int t = oldClassPos; t < oldFields.size(); t++) { if (oldFields.get(t).name.equals(newClassField.name)) { foundPosition = t; break; } } if (foundPosition == -1) { fieldChangeState = ChangeState.REMOVED; // Loop forward on old class until back in sync? } else { fieldChangeState = ChangeState.ADDED; } } ApiField fieldCopy = copyField(newClassField); fieldCopy.changeState = fieldChangeState; if (!fieldCopy.isPrimitive()) { fieldCopy.type = compareClasses(oldClassField.type, newClassField.type); } diffClass.addField(fieldCopy); } // run through any fields still left on Old for (int f = newFields.size(); f < oldFields.size(); f++) { ApiField fieldCopy = copyField(oldFields.get(f)); fieldCopy.changeState = ChangeState.REMOVED; diffClass.addField(fieldCopy); } return diffClass; } /** * Create a copy of a field, expect for the type */ private static ApiField copyField(ApiField apiField) { ApiField fieldCopy = new ApiField(); fieldCopy.name = apiField.name; fieldCopy.required = apiField.required; fieldCopy.primitiveType = apiField.primitiveType; fieldCopy.wrapperName = apiField.wrapperName; fieldCopy.description = apiField.description; fieldCopy.validValues = Lists.newArrayList(apiField.validValues); fieldCopy.collection = apiField.collection; fieldCopy.min = apiField.min; fieldCopy.max = apiField.max; return fieldCopy; } private static List<ApiField> findAddedFields(List<ApiField> oldList, List<ApiField> newList) { List<ApiField> addedField = Lists.newArrayList(); for (ApiField newField : newList) { if (!containsField(newField.name, oldList)) { addedField.add(newField); } } return addedField; } private static List<ApiField> findRemovedFields(List<ApiField> oldList, List<ApiField> newList) { List<ApiField> removedFields = Lists.newArrayList(); for (ApiField oldField : newList) { if (!containsField(oldField.name, newList)) { removedFields.add(oldField); } } return removedFields; } private static List<String> findRemovedValues(List<String> oldList, List<String> newList) { List<String> removedValues = Lists.newArrayList(); for (String oldValue : oldList) { if (!newList.contains(oldValue)) { removedValues.add(oldValue); } } return removedValues; } private static List<String> findAddedValues(List<String> oldList, List<String> newList) { List<String> addedValues = Lists.newArrayList(); for (String newValue : newList) { if (!oldList.contains(newValue)) { addedValues.add(newValue); } } return addedValues; } private static void findClassChanges(ApiClass oldClass, ApiClass newClass) { System.out.println("COMPARING CLASS " + oldClass.name); // Check for removed fields for (ApiField oldField : oldClass.fields) { if (!containsField(oldField.name, newClass.fields)) { System.out.println("Field REMOVED : " + oldField.name); } } // Check for removed fields for (ApiField newField : newClass.fields) { if (!containsField(newField.name, oldClass.fields)) { System.out.println("Field ADDED : " + newField.name); } } for (ApiField newField : newClass.fields) { ApiField oldField = findField(newField.name, oldClass.fields); if (oldField != null) { findClassChanges(oldField.type, newField.type); } } } public static boolean containsService(String className, List<ApiService> services) { return findService(className, services) != null; } private static ApiService findService(String className, List<ApiService> services) { for (ApiService service : services) { if (service.getFqJavaClassName().equals(className)) { return service; } } return null; } public static boolean containsMethod(String javaMethodName, List<ApiMethod> methods) { return findMethod(javaMethodName, methods) != null; } private static ApiMethod findMethod(String javaMethodName, List<ApiMethod> methods) { for (ApiMethod method : methods) { if (method.javaMethodName.equals(javaMethodName)) { return method; } } return null; } public static boolean containsField(String fieldName, List<ApiField> fields) { return findField(fieldName, fields) != null; } public static ApiField findField(String fieldName, List<ApiField> fields) { for (ApiField field : fields) { if (field.name.equals(fieldName)) { return field; } } return null; } /** * For more information on the LCS algorithm, see http://en.wikipedia.org/wiki/Longest_common_subsequence_problem */ private static int[][] computeLcs(List<ApiField> sequenceA, List<ApiField> sequenceB) { int[][] lcs = new int[sequenceA.size() + 1][sequenceB.size() + 1]; for (int i = 0; i < sequenceA.size(); i++) { for (int j = 0; j < sequenceB.size(); j++) { if (sequenceA.get(i).compareTo(sequenceB.get(j)) == 0) { lcs[i + 1][j + 1] = lcs[i][j] + 1; } else { lcs[i + 1][j + 1] = Math.max(lcs[i][j + 1], lcs[i + 1][j]); } } } return lcs; } public static ApiClass generateMergedClass(ApiClass oldClass, ApiClass newClass) { ApiClass mergedClass = new ApiClass(); if (oldClass == null) { throw new RuntimeException("Old Class NULL " + newClass.name); } if (newClass == null) { throw new RuntimeException("New Class NULL"); } mergedClass.name = newClass.name; mergedClass.fields = generateMergedFields(oldClass.fields, newClass.fields); return mergedClass; } /** * Generates a merged list with changes */ public static List<ApiField> generateMergedFields(List<ApiField> oldFields, List<ApiField> newFields) { int[][] lcs = computeLcs(oldFields, newFields); List<ApiField> mergedFields = Lists.newArrayList(); int aPos = oldFields.size(); int bPos = newFields.size(); while (aPos > 0 || bPos > 0) { if (aPos > 0 && bPos > 0 && oldFields.get(aPos - 1).compareTo(newFields.get(bPos - 1)) == 0) { ApiField field = oldFields.get(aPos - 1); field.changeState = ChangeState.NOT_CHANGED; mergedFields.add(field); aPos--; bPos--; } else if (bPos > 0 && (aPos == 0 || lcs[aPos][bPos - 1] >= lcs[aPos - 1][bPos])) { ApiField field = newFields.get(bPos - 1); field.changeState = ChangeState.ADDED; mergedFields.add(field); bPos--; } else { ApiField field = oldFields.get(aPos - 1); field.changeState = ChangeState.REMOVED; mergedFields.add(field); aPos--; } } // Backtracking generates the list from back to front, // so reverse it to get front-to-back. Collections.reverse(mergedFields); return mergedFields; } public static boolean containsChanges(List<ApiField> fields) { for (ApiField field : fields) { if (field.changeState != ChangeState.NOT_CHANGED) { return true; } } return false; } /** * @return Indicates if this class contains ANY changes (directly or within a fields type) */ public static boolean containsChanges(ApiClass apiClass) { if (apiClass == null) { return false; } for (ApiField field : apiClass.fields) { if (field.changeState != ChangeState.NOT_CHANGED) { return true; } } for (ApiField field : apiClass.fields) { if (!field.isPrimitive()) { boolean containsChanges = containsChanges(field.type); if (containsChanges) { return true; } } } return false; } }