/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */


package org.apache.aries.versioning.check;

import static org.apache.aries.versioning.utils.SemanticVersioningUtils.oneLineBreak;
import static org.apache.aries.versioning.utils.SemanticVersioningUtils.twoLineBreaks;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.aries.util.filesystem.FileSystem;
import org.apache.aries.util.filesystem.IDirectory;
import org.apache.aries.util.filesystem.IFile;
import org.apache.aries.util.io.IOUtils;
import org.apache.aries.util.manifest.ManifestHeaderProcessor;
import org.apache.aries.versioning.utils.BinaryCompatibilityStatus;
import org.apache.aries.versioning.utils.ClassDeclaration;
import org.apache.aries.versioning.utils.FieldDeclaration;
import org.apache.aries.versioning.utils.MethodDeclaration;
import org.apache.aries.versioning.utils.SemanticVersioningClassVisitor;
import org.apache.aries.versioning.utils.SemanticVersioningUtils;
import org.apache.aries.versioning.utils.SerialVersionClassVisitor;
import org.objectweb.asm.ClassReader;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @version $Rev:$ $Date:$
 */
public class BundleCompatibility {
  private static final Logger _logger = LoggerFactory.getLogger(BundleCompatibility.class);


  private URLClassLoader oldJarsLoader;
  private URLClassLoader newJarsLoader;
  private String bundleSymbolicName;
  private String bundleElement;
  private boolean bundleVersionCorrect;
  private BundleInfo currentBundle;
  private BundleInfo baseBundle;
  private StringBuilder pkgElements = new StringBuilder();
  private List<String> pkgElementsList = new ArrayList<String>();
  private List<String> excludes;

  private VersionChange bundleChange;
  private final Map<String, VersionChange> packageChanges = new HashMap<String, VersionChange>();

  public BundleCompatibility(String bundleSymbolicName, BundleInfo currentBundle, BundleInfo baseBundle, URLClassLoader oldJarsLoader, URLClassLoader newJarsLoader) {
    this(bundleSymbolicName,
         currentBundle,
         baseBundle,
         oldJarsLoader,
         newJarsLoader,
         null);
  }
  
  public BundleCompatibility(String bundleSymbolicName, BundleInfo currentBundle, BundleInfo baseBundle, URLClassLoader oldJarsLoader, URLClassLoader newJarsLoader, List<String> excludes) {
    this.bundleSymbolicName = bundleSymbolicName;
    this.currentBundle = currentBundle;
    this.baseBundle = baseBundle;
    this.oldJarsLoader = oldJarsLoader;
    this.newJarsLoader = newJarsLoader;
    this.excludes = excludes != null ? excludes : new ArrayList<String>();
  }

  public VersionChange getBundleChange() {
    return bundleChange;
  }

  public Map<String, VersionChange> getPackageChanges() {
    return packageChanges;
  }

  public String getBundleElement() {
    return bundleElement;
  }

  public StringBuilder getPkgElements() {
    return pkgElements;
  }
  
  private boolean ignoreChange(String reason) {
    if ((reason == null) || (this.excludes.isEmpty())) return false;
    
    for (String exclude : this.excludes) {
      // Could have interpreted each exclude as a regex, but that makes it easy to write loose rules
      // that match more strings than intended.
      if ((reason != null) && reason.contains(exclude)) return true; 
    }
    
    return false;
  }
  
  public boolean isBundleVersionCorrect() {
    return bundleVersionCorrect;
  }

  public BundleCompatibility invoke() throws IOException {
    String reason = null;
    // open the manifest and scan the export package and find the package name and exported version
    // The tool assume the one particular package just exports under one version
    Map<String, PackageContent> currBundleExpPkgContents = getAllExportedPkgContents(currentBundle);
    Map<String, PackageContent> baseBundleExpPkgContents;
    boolean pkg_major_change = false;
    boolean pkg_minor_change = false;
    String fatal_package = null;
    if (!currBundleExpPkgContents.isEmpty()) {
      baseBundleExpPkgContents = getAllExportedPkgContents(baseBundle);
      // compare each class right now
      for (Map.Entry<String, PackageContent> pkg : baseBundleExpPkgContents.entrySet()) {
        String pkgName = pkg.getKey();
        Map<String, IFile> baseClazz = pkg.getValue().getClasses();
        Map<String, IFile> baseXsds = pkg.getValue().getXsds();
        PackageContent currPkgContents = currBundleExpPkgContents.get(pkgName);
        if (currPkgContents == null) {
          // The package is no longer exported any more. This should lead to bundle major version change.
          pkg_major_change = true;
          fatal_package = pkgName;
          _logger.debug("The package " + pkgName + " in the bundle of " + bundleSymbolicName + " is no longer to be exported. Major change.");
        } else {
          Map<String, IFile> curClazz = currPkgContents.getClasses();
          Map<String, IFile> curXsds = currPkgContents.getXsds();
          //check whether there should be major change/minor change/micro change in this package.
          //1. Use ASM to visit all classes in the package
          VersionChangeReason majorChange = new VersionChangeReason();
          VersionChangeReason minorChange = new VersionChangeReason();
          // check all classes to see whether there are minor or major changes
          visitPackage(pkgName, baseClazz, curClazz, majorChange, minorChange);
          // If there is no binary compatibility changes, check whether xsd files have been added, changed or deleted
          if (!majorChange.isChange()) {
            checkXsdChangesInPkg(pkgName, baseXsds, curXsds, majorChange);
            // If everything is ok with the existing classes. Need to find out whether there are more API (abstract classes) in the current bundle.
            // loop through curClazz and visit it and find out whether one of them is abstract.
            // check whether there are more xsd or abstract classes added
            if (!(majorChange.isChange() || minorChange.isChange())) {
              checkAdditionalClassOrXsds(pkgName, curClazz, curXsds, minorChange);
            }
          }
          // We have scanned the whole packages, report the result
          //                    if (majorChange.isChange() || minorChange.isChange()) {
          String oldVersion = pkg.getValue().getPackageVersion();
          String newVersion = currPkgContents.getPackageVersion();
          if (majorChange.isChange() && !ignoreChange(majorChange.getReason())) {
            packageChanges.put(pkgName, new VersionChange(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion));
            pkg_major_change = true;
            fatal_package = pkgName;
            if (!isVersionCorrect(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion)) {
              pkgElementsList.add(getPkgStatusText(pkgName, VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion, majorChange.getReason(), majorChange.getChangeClass()));
            }
          } else if (minorChange.isChange() && !ignoreChange(minorChange.getReason())) {
            packageChanges.put(pkgName, new VersionChange(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion));
            pkg_minor_change = true;
            if (fatal_package == null) fatal_package = pkgName;
            if (!isVersionCorrect(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion)) {
              pkgElementsList.add(getPkgStatusText(pkgName, VERSION_CHANGE_TYPE.MINOR_CHANGE, pkg.getValue().getPackageVersion(), currPkgContents.getPackageVersion(), minorChange.getReason(), minorChange.getChangeClass()));
            }
          }  else {
            packageChanges.put(pkgName, new VersionChange(VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion));
            pkgElementsList.add(getPkgStatusText(pkgName, VERSION_CHANGE_TYPE.NO_CHANGE, pkg.getValue().getPackageVersion(), currPkgContents.getPackageVersion(), "", ""));
          }
      }
      }
      // If there is a package version change, the bundle version needs to be updated.
      // If there is a major change in one of the packages, the bundle major version needs to be increased.
      // If there is a minor change in one of the packages, the bundle minor version needs to be increased.
      String oldVersion = baseBundle.getBundleManifest().getVersion().toString();
      String newVersion = currentBundle.getBundleManifest().getVersion().toString();


      if (pkg_major_change || pkg_minor_change) {

        if (pkg_major_change) {
          // The bundle version's major value should be increased.
          bundleChange = new VersionChange(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion);
          reason = "Some packages have major changes. For an instance, the package " + fatal_package + " has major version changes.";
          bundleElement = getBundleStatusText(currentBundle.getBundle().getName(), bundleSymbolicName, VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion, reason);
          bundleVersionCorrect = isVersionCorrect(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion);
        } else if (pkg_minor_change) {
          bundleChange = new VersionChange(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion);
          reason = "Some packages have minor changes. For an instance, the package " + fatal_package + " has minor version changes.";
          bundleElement = getBundleStatusText(currentBundle.getBundle().getName(), bundleSymbolicName, VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion, reason);
          bundleVersionCorrect = isVersionCorrect(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion);
        }
      } else {
        bundleChange = new VersionChange(VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion);
        bundleVersionCorrect = isVersionCorrect(VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion);
        if (!bundleVersionCorrect) {
          reason = "The bundle has no version changes.";
          bundleElement = getBundleStatusText(currentBundle.getBundle().getName(), bundleSymbolicName, VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion, reason);
        }
      }
    }
    return this;
  }

  private Map<String, PackageContent> getAllExportedPkgContents(BundleInfo currentBundle) {
    String packageExports = currentBundle.getBundleManifest().getRawAttributes().getValue(Constants.EXPORT_PACKAGE);
    List<ManifestHeaderProcessor.NameValuePair> exportPackageLists = ManifestHeaderProcessor.parseExportString(packageExports);
    // only perform validation if there are some packages exported. Otherwise, not interested.
    Map<String, PackageContent> exportedPackages = new HashMap<String, PackageContent>();
    if (!exportPackageLists.isEmpty()) {
      File bundleFile = currentBundle.getBundle();
      IDirectory bundleDir = FileSystem.getFSRoot(bundleFile);
      for (ManifestHeaderProcessor.NameValuePair exportedPackage : exportPackageLists) {
        String packageName = exportedPackage.getName();
        String packageVersion = exportedPackage.getAttributes().get(Constants.VERSION_ATTRIBUTE);
        // need to go through each package and scan every class
        exportedPackages.put(packageName, new PackageContent(packageName, packageVersion));
      }
      // scan the jar and list all the files under each package
      List<IFile> allFiles = bundleDir.listAllFiles();
      for (IFile file : allFiles) {
        String directoryFullPath = file.getName();
        String directoryName = null;
        String fileName = null;
        if (file.isFile() && ((file.getName().endsWith(SemanticVersioningUtils.classExt) || (file.getName().endsWith(SemanticVersioningUtils.schemaExt))))) {
          if (directoryFullPath.lastIndexOf("/") != -1) {
            directoryName = directoryFullPath.substring(0, directoryFullPath.lastIndexOf("/"));
            fileName = directoryFullPath.substring(directoryFullPath.lastIndexOf("/") + 1);
          }
        }

        if (directoryName != null) {
          String pkgName = directoryName.replaceAll("/", ".");
          PackageContent pkgContent = exportedPackages.get(pkgName);
          if (pkgContent != null) {
            if (file.getName().endsWith(SemanticVersioningUtils.classExt)) {
              pkgContent.addClass(fileName, file);
            } else {
              pkgContent.addXsd(fileName, file);
            }
            exportedPackages.put(pkgName, pkgContent);
          }
        }
      }
    }
    return exportedPackages;
  }

  private String getBundleStatusText(String bundleFileName, String bundleSymbolicName, VERSION_CHANGE_TYPE status, String oldVersionStr, String newVersionStr, String reason) {
    if (!isVersionCorrect(status, oldVersionStr, newVersionStr)) {
      return "The bundle " + bundleSymbolicName + " has the following changes:\r\n" + reason + "\r\nThe bundle version should be " + getRecommendedVersion(status, oldVersionStr) + ".";
    } else {
      return "";
    } 


  }

  /**
   * Visit the whole package to scan each class to see whether we need to log minor or major changes.
   *
   * @param pkgName
   * @param baseClazz
   * @param curClazz
   * @param majorChange
   * @param minorChange
   */
  private void visitPackage(String pkgName, Map<String, IFile> baseClazz,
      Map<String, IFile> curClazz, VersionChangeReason majorChange, VersionChangeReason minorChange) {
    StringBuilder major_reason = new StringBuilder();
    StringBuilder minor_reason = new StringBuilder();
    boolean is_major_change = false;
    boolean is_minor_change = false;
    String fatal_class = null;
    boolean foundNewAbstract = false;
    for (Map.Entry<String, IFile> file : baseClazz.entrySet()) {
      // scan the latest version of the class
      IFile curFile = curClazz.get(file.getKey());
      String changeClass = file.getValue().getName();
      //Scan the base version
      SemanticVersioningClassVisitor oldcv = getVisitor(file.getValue(), oldJarsLoader);
      // skip the property files as they are compiled as class file as well
      ClassDeclaration cd = oldcv.getClassDeclaration();
      if ((cd != null) && (!SemanticVersioningUtils.isPropertyFile(cd))) {

        if (curFile == null) {
          // the class we are scanning has been deleted from the current version of the bundle
          // This should be a major increase
          major_reason.append(twoLineBreaks + "The class/interface " + getClassName(changeClass) + " has been deleted from the package.");
          //majorChange.update(reason, changeClass);
          is_major_change = true;
          // only replace the fatal class if not set as the class won't be found in cmvc due to the fact it has been deleted.
          if (fatal_class == null) {
            fatal_class = changeClass;
          }
        } else {
          // check for binary compatibility
          // load the class from the current version of the bundle
          // remove it from the curClazz collection as we would like to know whether there are more classes added
          curClazz.remove(file.getKey());
          SemanticVersioningClassVisitor newcv = getVisitor(curFile, newJarsLoader);
          // check for binary compatibility
          ClassDeclaration newcd = newcv.getClassDeclaration();
          BinaryCompatibilityStatus bcs = newcd.getBinaryCompatibleStatus(oldcv.getClassDeclaration());

          if (!bcs.isCompatible()) {
            major_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following changes have been made since the last release.");
            // break binary compatibility
            for (String reason : bcs) {
              major_reason.append(oneLineBreak).append(reason);
            }
            is_major_change = true;
            fatal_class = changeClass;
          } else {
            //check to see whether more methods are added
            ClassDeclaration oldcd = oldcv.getClassDeclaration();
            Collection<MethodDeclaration> extraMethods = newcd.getExtraMethods(oldcd);

            boolean containsConcrete = false;
            boolean containsAbstract = false;

            boolean abstractClass = newcd.isAbstract();

            StringBuilder subRemarks = new StringBuilder();
            String concreteSubRemarks = null;
            for (MethodDeclaration extraMethod : extraMethods) {
              //only interested in the visible methods not the system generated ones
              if (!extraMethod.getName().contains("$")) {
                if (abstractClass) {
                  if (extraMethod.isAbstract()) {
                    foundNewAbstract = true;
                    containsAbstract = true;
                    subRemarks.append(oneLineBreak + SemanticVersioningUtils.getReadableMethodSignature(extraMethod.getName(), extraMethod.getDesc()));
                  } else {
                    //only list one abstract method, no need to list all
                    containsConcrete = true;
                    concreteSubRemarks = oneLineBreak + SemanticVersioningUtils.getReadableMethodSignature(extraMethod.getName(), extraMethod.getDesc());
                  }
                } else {
                  containsConcrete = true;
                  concreteSubRemarks = oneLineBreak + SemanticVersioningUtils.getReadableMethodSignature(extraMethod.getName(), extraMethod.getDesc());
                  break;
                }
              }
            }

            if (containsConcrete || containsAbstract) {
              is_minor_change = true;
              if (!is_major_change) {
                fatal_class = changeClass;
              }
              if (containsAbstract) {

                minor_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following abstract methods have been added since the last release of this bundle.");
                minor_reason.append(subRemarks);
              } else {
                minor_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following method has been added since the last release of this bundle.");
                minor_reason.append(concreteSubRemarks);
              }
            }
            //check to see whether there are extra public/protected fields if there is no additional methods

            if (!is_minor_change) {
              for (FieldDeclaration field : newcd.getExtraFields(oldcd)) {
                if (field.isPublic() || field.isProtected()) {
                  is_minor_change = true;
                  String extraFieldRemarks = oneLineBreak + " " + SemanticVersioningUtils.transform(field.getDesc()) + " " + field.getName();
                  if (!is_major_change) {
                    fatal_class = changeClass;
                  }
                  minor_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following fields have been added since the last release of this bundle.");
                  minor_reason.append(extraFieldRemarks);
                  break;
                }
              }

            }

          }
        }
      }
    }
    if (is_major_change) {
      majorChange.update(major_reason.toString(), fatal_class, false);
    }
    if (is_minor_change) {
      minorChange.update(minor_reason.toString(), fatal_class, (foundNewAbstract ? true : false));
    }
  }

  /**
   * Check whether the package has xsd file changes or deleted. If yes, log a minor change.
   *
   * @param pkgName
   * @param baseXsds
   * @param curXsds
   * @param majorChange
   * @throws java.io.IOException
   */

  private void checkXsdChangesInPkg(String pkgName, Map<String, IFile> baseXsds,
      Map<String, IFile> curXsds, VersionChangeReason majorChange) throws IOException {
    String reason;
    for (Map.Entry<String, IFile> file : baseXsds.entrySet()) {
      // scan the latest version of the class
      IFile curXsd = curXsds.get(file.getKey());
      String changeClass = file.getValue().getName();
      // check whether the xsd have been deleted or changed or added
      if (curXsd == null) {
        reason = "In the package " + pkgName + ", The schema file has been deleted: " + file.getKey() + ".";
        majorChange.update(reason, changeClass, false);
        break;
      } else {
        // check whether it is the same
        //read the current xsd file
        curXsds.remove(file.getKey());
        String curFileContent = readXsdFile(curXsd.open());
        String oldFileContent = readXsdFile(file.getValue().open());
        if (!(curFileContent.equals(oldFileContent))) {

          reason = "In the package " + pkgName + ", The schema file has been updated: " + file.getKey() + ".";
          majorChange.update(reason, changeClass, false);
          break;
        }
      }
    }
  }

  /**
   * Check whether the package has gained additional class or xsd files. If yes, log a minor change.
   *
   * @param pkgName
   * @param curClazz
   * @param curXsds
   * @param minorChange
   */
  private void checkAdditionalClassOrXsds(String pkgName, Map<String, IFile> curClazz,
      Map<String, IFile> curXsds, VersionChangeReason minorChange) {
    String reason;
    Collection<IFile> ifiles = curClazz.values();
    Iterator<IFile> iterator = ifiles.iterator();
    while (iterator.hasNext()) {
      IFile ifile = iterator.next();
      String changeClass = ifile.getName();
      SemanticVersioningClassVisitor cv = getVisitor(ifile, newJarsLoader);
      if (cv.getClassDeclaration() != null) {
        // If this is a public/protected class, it will need to increase the minor version of the package.
        minorChange.setChange(true);
        if (minorChange.isChange()) {
          reason = "The package " + pkgName + " has gained at least one class : " + getClassName(changeClass) + ".";
          minorChange.update(reason, changeClass, false);
          break;
        }
      }
    }
    if (!(minorChange.isChange() || curXsds.isEmpty())) {
      /// a new xsd file was added, it is a minor change
      IFile firstXsd = null;
      Iterator<IFile> xsdIterator = curXsds.values().iterator();
      firstXsd = xsdIterator.next();

      reason = "In the package " + pkgName + ", The schema file(s) are added: " + curXsds.keySet() + ".";
      minorChange.update(reason, firstXsd.getName(), false);
    }
  }

  static boolean isVersionCorrect(VERSION_CHANGE_TYPE status, String oldVersionStr, String newVersionStr) {
    boolean versionCorrect = false;

    Version oldVersion = Version.parseVersion(oldVersionStr);
    Version newVersion = Version.parseVersion(newVersionStr);

    if (status == VERSION_CHANGE_TYPE.MAJOR_CHANGE) {
      if (newVersion.getMajor() > oldVersion.getMajor()) {
        versionCorrect = true;
      }
    } else if (status == VERSION_CHANGE_TYPE.MINOR_CHANGE) {
      if ((newVersion.getMajor() > oldVersion.getMajor()) || (newVersion.getMinor() > oldVersion.getMinor())) {
        versionCorrect = true;
      }
    } else {
      if ((newVersion.getMajor() >= oldVersion.getMajor()) && (newVersion.getMinor() >= oldVersion.getMinor())) {
        versionCorrect = true;
      }
    }
    return versionCorrect;
  }
  private String getRecommendedVersion( VERSION_CHANGE_TYPE status, String oldVersionStr) {
    Version oldVersion = Version.parseVersion(oldVersionStr);
    Version recommendedNewVersion;

    if (status == BundleCompatibility.VERSION_CHANGE_TYPE.MAJOR_CHANGE) {
      recommendedNewVersion = new Version(oldVersion.getMajor() + 1, 0, 0);
    } else if (status == BundleCompatibility.VERSION_CHANGE_TYPE.MINOR_CHANGE) {
      recommendedNewVersion = new Version(oldVersion.getMajor(), oldVersion.getMinor() + 1, 0);
    } else {
      recommendedNewVersion = oldVersion;
    }
    return recommendedNewVersion.toString();
  }

  private String getPkgStatusText(String pkgName, VERSION_CHANGE_TYPE status, String oldVersionStr, String newVersionStr, String reason, String key_class) {




    if (!isVersionCorrect(status, oldVersionStr, newVersionStr)) {
      return "The package " + pkgName + " has the following changes:" + reason + "\r\nThe package version should be " + getRecommendedVersion(status, oldVersionStr) + ".";
    } else {
      return "";
    }

  }



  private String getClassName(String fullClassPath) {
    String[] chunks = fullClassPath.split("/");
    String className = chunks[chunks.length - 1];
    className = className.replace(SemanticVersioningUtils.classExt, SemanticVersioningUtils.javaExt);
    return className;
  }

  private String readXsdFile(InputStream is) {
    BufferedReader br = new BufferedReader(new InputStreamReader(is));
    StringBuilder sb = new StringBuilder();
    String line = null;
    try {
      while ((line = br.readLine()) != null) {
        sb.append(line);
      }
    } catch (IOException ioe) {
      IOUtils.close(br);
    }
    return sb.toString();
  }

  private SemanticVersioningClassVisitor getVisitor(IFile file, URLClassLoader loader) {
   
    SerialVersionClassVisitor sv = new SerialVersionClassVisitor(null);
    SemanticVersioningClassVisitor oldcv = new SemanticVersioningClassVisitor(loader, sv);
    try {
      ClassReader cr = new ClassReader(file.open());
      cr.accept(oldcv, 0);
    } catch (IOException ioe) {
      _logger.debug("The file " + file + "cannot be opened.");
    }
    return oldcv;
  }

  enum VERSION_CHANGE_TYPE {
    MAJOR_CHANGE("major"), MINOR_CHANGE("minor"), NO_CHANGE("no");
    private final String text;

    VERSION_CHANGE_TYPE(String text) {
      this.text = text;
    }

    public String text() {
      return this.text;
    }

  }

  private static class PackageContent {
    private final String packageName;
    private final String packageVersion;
    private final Map<String, IFile> classes = new HashMap<String, IFile>();
    private final Map<String, IFile> xsds = new HashMap<String, IFile>();

    PackageContent(String pkgName, String pkgVersion) {
      packageName = pkgName;
      packageVersion = pkgVersion;
    }

    public void addClass(String className, IFile file) {
      classes.put(className, file);
    }

    public void addXsd(String className, IFile file) {
      xsds.put(className, file);
    }

    public Map<String, IFile> getClasses() {
      return classes;
    }

    public Map<String, IFile> getXsds() {
      return xsds;
    }

    public String getPackageVersion() {
      return packageVersion;
    }

    public String getPackageName() {
      return packageName;
    }
  }

  private static class VersionChangeReason {
    boolean change = false;
    String reason = null;
    String changeClass = null;
    boolean moreAbstractMethod = false;


    public boolean isMoreAbstractMethod() {
      return moreAbstractMethod;
    }

    public boolean isChange() {
      return change;
    }

    public void setChange(boolean change) {
      this.change = change;
    }

    public String getReason() {
      return reason;
    }

    public String getChangeClass() {
      return changeClass;
    }

    public void update(String reason, String changeClass, boolean moreAbstractMethod) {
      this.change = true;
      this.reason = reason;
      this.changeClass = changeClass;
      this.moreAbstractMethod = moreAbstractMethod;
    }
  }

}
