public class ArtifactCoordinates {
  private String groupId;
  private String artifactId;
  private String version;
  private String type;
  private String classifier;

  public ArtifactCoordinates(String groupId, String artifactId, String version) {
    this(groupId, artifactId, version, null, null);
  }

  public ArtifactCoordinates(String groupId, String artifactId, String version, String type) {
    this(groupId, artifactId, version, type, null);
  }

  public ArtifactCoordinates(String groupId, String artifactId, String version, String type, String classifier) {
    this.groupId = groupId;
    this.artifactId = artifactId;
    this.version = version;
    this.type = type;
    this.classifier = classifier;
  }

  public String getGroupId() {
    return this.groupId;
  }

  public String getArtifactId() {
    return this.artifactId;
  }

  public String getVersion() {
    return this.version;
  }

  public String getType() {
    return this.type;
  }

  public String getClassifier() {
    return this.classifier;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(this.groupId).append(':');
    sb.append(this.artifactId).append(':');
    if (this.type != null) {
      sb.append(this.type).append(':');
    }
    if (this.classifier != null) {
      sb.append(this.classifier).append(':');
    }
    sb.append(this.version);
    return sb.toString();
  }

  @Override
  public boolean equals(Object other) {
    if (other == null) {
      return false;
    }
    if (!(other instanceof ArtifactCoordinates)) {
      return false;
    }
    return Objects.equal(toString(), other.toString());
  }

  public boolean equalsGAV(Object other) {
    if (other == null) {
      return false;
    }
    if (!(other instanceof ArtifactCoordinates)) {
      return false;
    }

    ArtifactCoordinates otherCoordinates = (ArtifactCoordinates) other;
    return Objects.equal(getArtifactId(), otherCoordinates.getArtifactId())
        && Objects.equal(getGroupId(), otherCoordinates.getGroupId())
        && Objects.equal(getVersion(), otherCoordinates.getVersion());
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.groupId, this.artifactId, this.type, this.classifier, this.version);
  }
} public enum ReleasePhase {
  PRE_RELEASE, RELEASE, POST_RELEASE;
} public interface ScmProvider {
  /**
   * Initializes the SCM provider with the working directory and some other optional parameters such as credentials.
   *
   * @param initialization contains some initialization details such as the working directory or authentication details.
   */
  void initialize(ScmProviderInitialization initialization);

  /**
   * Closes the SCM provider and releases all bound resources.
   */
  void close();

  /**
   * Checks out a remote repository into the working directory which has been set during provider initialization.<br>
   * The checkout can be performed from any branch or tag with optional specification of revision information.<br>
   * <br>
   * <b>Note that the working directory of this provider instance must be empty for checkout operations!</b>
   *
   * @param request the description of what to checkout from where.
   * @throws ScmException if f.i. the working dir is not empty or anything else happens during checkout.
   */
  void checkout(CheckoutRequest request) throws ScmException;

  /**
   * Commits the specified paths or the whole working directory changes. For distributed SCMs a push to the remote
   * repository can be specified. If there are remote changes the provider tries to commit the local changes or fails if
   * there are conflicts.
   *
   * @param request the request containing all relevant settings for the commit.
   * @return the new revision number after the commit which is the remote revision or the new local revision for
   *         distributed SCMs when pushing is not enabled.
   * @throws ScmException if either the commit or the push to the remote repo encountered an error.
   */
  String commit(CommitRequest request) throws ScmException;

  /**
   * Pushes the local changes to the remote repository which is relevant for distributed SCMs only.
   *
   * @param request the request containing all relevant settings for the push.
   * @return the new remote revision after the push has been executed successfully.
   * @throws ScmException if the push encountered an error, f.i. if the remote repo is ahead, ...
   */
  String push(PushRequest request) throws ScmException;

  /**
   * Updates the local repository with changes of the remote repository which might fail due to conflicts. Merging can
   * also be required.
   *
   * @param request the request describing what to update to which revision and how to deal with conflicts.
   * @return the new revision after merging remote changes. This might be the requested remote revision which has been
   *         merged or a newer one if a merge commit was necessary.
   * @throws ScmException if the update fails, f.i. due to unresolvable conflicts.
   */
  String update(UpdateRequest request) throws ScmException;

  /**
   * Creates a tag on the local (and optionally the remote) repository either from the working copy or from a specified
   * remote repository location.
   *
   * @param request the request specifying all relevant information for the tag creation.
   * @return the new revision number after the tag has been created which is the remote revision or the new local
   *         revision for distributed SCMs when pushing is not enabled.
   * @throws ScmException if the tag could not be created.
   */
  String tag(TagRequest request) throws ScmException;

  /**
   * @param tagName the name of the tag which is searched.
   * @return <code>true</code> if the repository contains a tag with the specified name.
   * @throws ScmException if the repository access (local or remote) fails and no exact information about the presence
   *           of the tag can be given.
   */
  boolean hasTag(String tagName) throws ScmException;

  /**
   * Deletes a tag from the repository. In case of distributed SCMs the deletion can only happen locally or also on the
   * remote repository if pushing is enabled.
   *
   * @param request the deletion request specifying all relevant information for tag deletion.
   * @return the new revision number after the tag deletion which is the remote revision or the new local revision for
   *         distributed SCMs when pushing is not enabled.
   * @throws ScmException if the tag could not be deleted.
   */
  String deleteTag(DeleteTagRequest request) throws ScmException;

  /**
   * Creates a branch from the local (and optionally the remote) repository either from the working copy or from a
   * specified remote repository location.
   *
   * @param request the request specifying all relevant information for the branch creation.
   * @return the new revision number after the branch has been created which is the remote revision or the new local
   *         revision for distributed SCMs when pushing is not enabled.
   * @throws ScmException if the branch could not be created.
   */
  String branch(BranchRequest request) throws ScmException;

  /**
   * @param branchName the name of the branch which is searched.
   * @return <code>true</code> if the repository contains a branch with the specified name.
   * @throws ScmException if the repository access (local or remote) fails and no exact information about the presence
   *           of the branch can be given.
   */
  boolean hasBranch(String branchName) throws ScmException;

  /**
   * Deletes a branch from the repository. In case of distributed SCMs the deletion can only happen locally or also on
   * the
   * remote repository if pushing is enabled.
   *
   * @param request the deletion request specifying all relevant information for branch deletion.
   * @return the new revision number after the branch deletion which is the remote revision or the new local revision
   *         for
   *         distributed SCMs when pushing is not enabled.
   * @throws ScmException if the branch could not be deleted.
   */
  String deleteBranch(DeleteBranchRequest request) throws ScmException;

  /**
   * Reverts a number of commits in local and remote repository. If the commits to revert are older than the actual HEAD
   * the diff gets merged on top of the HEAD revision.
   *
   * @param request the request containing the revision range to be reverted and merge information.
   * @return the latest remote revision on the current branch.
   * @throws ScmException if f.i. the specified revisions are newer than the HEAD or FROM and TO are specified in the
   *           wrong order or anything happens during the reversion.
   */
  String revertCommits(RevertCommitsRequest request) throws ScmException;

  /**
   * @return the revision of the current working copy.
   */
  String getLocalRevision();

  /**
   * @return the latest remote revision. In case of distributed SCMs the remote revision is retrieved from the remote
   *         the current branch is tracking.
   */
  String getLatestRemoteRevision();

  /**
   * Calculates the URL of a tag with the given tag name from another connection URL which might just be a basic
   * URL or a URL containing branch info, ...
   *
   * @param currentConnectionString a connection URL from which the tag URL shall be derived assuming standard
   *          directory layout.
   * @param tagName the name of the branch.
   * @return the calculated tag URL.
   */
  String calculateTagConnectionString(String currentConnectionString, String tagName);

  /**
   * Calculates the URL of the branch with the given branch name from another connection URL which might just be a basic
   * URL or a URL containing branch info, ...
   *
   * @param currentConnectionString a connection URL from which the branch URL shall be derived assuming standard
   *          directory layout.
   * @param branchName the name of the branch.
   * @return the calculated branch URL.
   */
  String calculateBranchConnectionString(String currentConnectionString, String branchName);

  /**
   * @return <code>true</code> if the SCM connection URL comprises also the tag name (and path).
   */
  boolean isTagInfoIncludedInConnection();

  /**
   * Calculates the history of the repository which can happen locally only remote-only or combined. Through the request
   * a variety of query conditions can be specified for smaller history results and faster queries, such as a commit
   * range or the maximum number of results.
   *
   * @param request the history request for specifying the history conditions.
   * @return the history produced by the query.
   * @throws ScmException if the SCM provider implementation encountered any error querying the repository history.
   */
  HistoryResult getHistory(HistoryRequest request) throws ScmException;

  /**
   * Calculates a diff between the requested repository paths and objects. The result may only contain diff status
   * information or a full textual diff for each changed object.
   *
   * @param request the request which specifies the diff calculation requirements.
   * @return a result containing a diff object for each changed object of the repository.
   * @throws ScmException if the underlying scm provider implementation encountered an error while creating the diff.
   *           The operation type is alway DIFF.
   */
  DiffResult getDiff(DiffRequest request) throws ScmException;
} public class RevertCommitsRequest {
  private String fromRevision;
  private String toRevision;
  private String message;
  private boolean push;
  private MergeStrategy mergeStrategy = MergeStrategy.DO_NOT_MERGE;
  private MergeClient mergeClient;

  private RevertCommitsRequest() {
    // use builder!
  }

  public static Builder builder() {
    return new Builder();
  }

  public String getFromRevision() {
    return this.fromRevision;
  }

  public String getToRevision() {
    return this.toRevision;
  }

  public String getMessage() {
    return this.message;
  }

  public boolean push() {
    return this.push;
  }

  public MergeStrategy getMergeStrategy() {
    return this.mergeStrategy;
  }

  public Optional<MergeClient> getMergeClient() {
    return Optional.fromNullable(this.mergeClient);
  }

  /**
   * The builder for a {@link RevertCommitsRequest}.
   *
   * @author <a href="mailto:stanley.hillner@itemis.de">Stanley Hillner</a>
   * @since 0.1.0
   */
  public static class Builder {
    private RevertCommitsRequest request = new RevertCommitsRequest();

    /**
     * @param revision the revision to which commits shall be reverted. The contents of this revision will be kept in
     *          the repository.<br>
     *          This revision must be older than the one specified in {@link #fromRevision(String)}. {@link ScmProvider}
     *          implementations may throw an exception when violating this constraint.
     * @return the builder itself.
     */
    public Builder toRevision(String revision) {
      this.request.toRevision = revision;
      return this;
    }

    /**
     * @param revision the revision from which to start reverting commits. All commits are reverted starting from this
     *          one until the revision specified in {@link #toRevision(String)}.<br>
     *          This revision must be newer than the one specified in {@link #toRevision(String)}. {@link ScmProvider}
     *          implementations may throw an exception when violating this constraint.
     * @return the builder itself.
     */
    public Builder fromRevision(String revision) {
      this.request.fromRevision = revision;
      return this;
    }

    /**
     * @param message the repository log message (mandatory).
     * @return the builder itself.
     */
    public Builder message(String message) {
      this.request.message = message;
      return this;
    }

    /**
     * Request pushing to the remote repository in case of distributed SCMs.
     *
     * @return the builder itself.
     */
    public Builder push() {
      this.request.push = true;
      return this;
    }

    /**
     * Sets the merge strategy to {@link MergeStrategy#USE_LOCAL} for merge conflicts during the reversion.<br>
     * This will request overriding of all conflicting changes with the local versions.
     *
     * @return the builder itself.
     */
    public Builder mergeUseLocal() {
      this.request.mergeStrategy = MergeStrategy.USE_LOCAL;
      return this;
    }

    /**
     * Sets the merge strategy to {@link MergeStrategy#USE_REMOTE} for merge conflicts during the reversion.<br>
     * This will request overriding of all conflicting changes with the remote versions.
     *
     * @return the builder itself.
     */
    public Builder mergeUseRemote() {
      this.request.mergeStrategy = MergeStrategy.USE_REMOTE;
      return this;
    }

    /**
     * Sets the merge strategy to {@link MergeStrategy#FULL_MERGE} for merge conflicts during the reversion.<br>
     * This will request real merging of conflicts between local and remote changes. In case of such a conflict the
     * {@link MergeClient} is used to resolve the conflict.<br>
     * <br>
     * Set the merge client using {@link #mergeClient(MergeClient)} when setting this merge strategy!
     *
     * @return the builder itself.
     */
    public Builder merge() {
      this.request.mergeStrategy = MergeStrategy.FULL_MERGE;
      return this;
    }

    /**
     * Sets the merge strategy to {@link MergeStrategy#DO_NOT_MERGE} for merge conflicts during the reversion.<br>
     * This will request to not merge local and remote changes which will likely result in failure messages or conflict
     * info being written.
     *
     * @return the builder itself.
     */
    public Builder noMerge() {
      this.request.mergeStrategy = MergeStrategy.DO_NOT_MERGE;
      return this;
    }

    /**
     * Sets a specific merge strategy for merge conflicts during the reversion.
     *
     * @param mergeStrategy the requested merge strategy. {@code null} results in {@link MergeStrategy#DO_NOT_MERGE}.
     * @return the builder itself.
     */
    public Builder mergeStrategy(MergeStrategy mergeStrategy) {
      if (mergeStrategy != null) {
        this.request.mergeStrategy = mergeStrategy;
      } else {
        noMerge();
      }
      return this;
    }

    /**
     * @param mergeClient the merge client to be used in case of merge conflicts and merge strategy
     *          {@link MergeStrategy#FULL_MERGE}.
     * @return the builder itself.
     */
    public Builder mergeClient(MergeClient mergeClient) {
      this.request.mergeClient = mergeClient;
      return this;
    }

    /**
     * Checks the settings of the request to build and builds the actual update request.
     *
     * @return the request for updating the local working copy.
     */
    public RevertCommitsRequest build() {
      Preconditions.checkState(this.request.fromRevision != null,
          "No from revision set (start revision of the reversion)!");
      Preconditions.checkState(this.request.toRevision != null,
          "No to revision set (end revision of the reversion that will be kept)!");
      Preconditions.checkState(this.request.message != null, "No log message specified!");
      if (MergeStrategy.FULL_MERGE == this.request.mergeStrategy) {
        Preconditions.checkState(this.request.mergeClient != null,
            "Merge strategy " + this.request.mergeStrategy + " has been requested but no merge client is set!");
      }
      return this.request;
    }
  }
} public class DevVersionUtil {
  @Inject
  private Logger log;
  @Inject
  private MavenProject project;
  @Inject
  @Named("reactorProjects")
  private List<MavenProject> reactorProjects;
  @Inject
  private ReleaseMetadata metadata;
  @Inject
  @Named("scmMessagePrefix")
  private String scmMessagePrefix;
  @Inject
  private ScmProviderRegistry scmProviderRegistry;
  private ScmProvider scmProvider;

  @PostConstruct
  private void init() {
    this.scmProvider = this.scmProviderRegistry.getProvider();
  }

  public void revertScmSettings(MavenProject projectToRevert, Document document) {
    Scm scm = this.metadata.getCachedScmSettings(projectToRevert);
    if (scm != null) {
      this.log.debug("\t\tReversion of SCM connection tags");
      Document originalPOM = this.metadata.getCachedOriginalPOM(projectToRevert);

      Node scmNode = PomUtil.getOrCreateScmNode(document, false);
      Node originalScmNode = PomUtil.getOrCreateScmNode(originalPOM, false);

      if (scmNode != null) {
        Optional<String> connection = PomUtil.getChildNodeTextContent(originalScmNode,
            PomUtil.NODE_NAME_SCM_CONNECTION);
        if (connection.isPresent()) {
          PomUtil.setNodeTextContent(scmNode, PomUtil.NODE_NAME_SCM_CONNECTION, connection.get(), false);
        }

        Optional<String> devConnection = PomUtil.getChildNodeTextContent(originalScmNode,
            PomUtil.NODE_NAME_SCM_DEV_CONNECTION);
        if (devConnection.isPresent()) {
          PomUtil.setNodeTextContent(scmNode, PomUtil.NODE_NAME_SCM_DEV_CONNECTION, devConnection.get(), false);
        }

        Optional<String> url = PomUtil.getChildNodeTextContent(originalScmNode, PomUtil.NODE_NAME_SCM_URL);
        if (url.isPresent()) {
          PomUtil.setNodeTextContent(scmNode, PomUtil.NODE_NAME_SCM_URL, url.get(), false);
        }

        if (scm.getTag() != null) {
          PomUtil.setNodeTextContent(scmNode, PomUtil.NODE_NAME_SCM_TAG, scm.getTag(), false);
        } else {
          PomUtil.deleteNode(scmNode, PomUtil.NODE_NAME_SCM_TAG);
        }
      }
    }
  }

  public void commitChanges(boolean commitPomsOnly) {
    this.log.debug(
        "\tCommitting changed POMs of all modules and pushing to remote repository. Merging with remote changes if necessary.");
    this.metadata.setScmRevisionBeforeNextDevVersion(this.scmProvider.getLatestRemoteRevision());

    StringBuilder message = new StringBuilder("Preparation for next development cycle.");
    if (StringUtils.isNotBlank(this.scmMessagePrefix)) {
      message.insert(0, this.scmMessagePrefix);
    }

    Builder requestBuilder = CommitRequest.builder().merge().mergeClient(new ScmPomVersionsMergeClient())
        .message(message.toString()).push();
    FileToRelativePath pathConverter = new FileToRelativePath(this.project.getBasedir());
    if (commitPomsOnly) {
      for (MavenProject p : this.reactorProjects) {
        requestBuilder.addPaths(pathConverter.apply(p.getFile()));
      }
    }

    String newRevision = this.scmProvider.commit(requestBuilder.build());
    this.metadata.setScmRevisionAfterNextDevVersion(newRevision);
  }
} public final class PomUtil {
  public static final String ARTIFACT_TYPE_JAR = "jar";
  public static final String ARTIFACT_TYPE_POM = "pom";
  public static final String VERSION_QUALIFIER_SNAPSHOT = "-SNAPSHOT";
  public static final String VERSION_LATEST = "LATEST";

  // DOM node names
  public static final String NODE_NAME_ARTIFACT_ID = "artifactId";
  public static final String NODE_NAME_BUILD = "build";
  public static final String NODE_NAME_EXECUTION = "execution";
  public static final String NODE_NAME_EXECUTIONS = "executions";
  public static final String NODE_NAME_GOAL = "goal";
  public static final String NODE_NAME_GOALS = "goals";
  public static final String NODE_NAME_GROUP_ID = "groupId";
  public static final String NODE_NAME_ID = "id";
  public static final String NODE_NAME_PARENT = "parent";
  public static final String NODE_NAME_PHASE = "phase";
  public static final String NODE_NAME_PLUGIN = "plugin";
  public static final String NODE_NAME_PLUGINS = "plugins";
  public static final String NODE_NAME_PROJECT = "project";
  public static final String NODE_NAME_SCM = "scm";
  public static final String NODE_NAME_SCM_CONNECTION = "connection";
  public static final String NODE_NAME_SCM_DEV_CONNECTION = "developerConnection";
  public static final String NODE_NAME_SCM_TAG = "tag";
  public static final String NODE_NAME_SCM_URL = "url";
  public static final String NODE_NAME_VERSION = "version";
  public static final String NODE_NAME_DEPENDENCY = "dependency";
  public static final String NODE_NAME_DEPENDENCIES = "dependencies";
  public static final String NODE_NAME_DEPENDENCY_MANAGEMENT = "dependencyManagement";

  private static final byte[] LINE_SEPERATOR = StandardSystemProperty.LINE_SEPARATOR.value().getBytes();

  private PomUtil() {
    // Should not be instanciated
  }

  /**
   * Parses the POM file from which the project was loaded into a {@link Document} for further manipulation.
   *
   * @param project the project from which the parser retrieves the POM file.
   * @return the parsed document for further manipulation.
   */
  public static final Optional<Document> parsePOM(MavenProject project) {
    try {
      return parsePOM(project.getFile());
    } catch (RuntimeException e) {
      throw new RuntimeException(
          "Could not load the project object model of the following module: " + ProjectToString.INSTANCE.apply(project),
          e);
    }
  }

  /**
   * Assumes that the passed file is a Maven POM file and parses it into a {@link Document} for further manipulation.
   *
   * @param pomFile the pom file to be parsed.
   * @return the parsed document for further manipulation.
   */
  public static final Optional<Document> parsePOM(File pomFile) {
    // adaption for https://github.com/shillner/unleash-maven-plugin/issues/98
    // in case of pom-less tycho builds the modules don't have POMs
    if (pomFile == null || !pomFile.exists()) {
      return Optional.absent();
    }

    Preconditions.checkArgument(pomFile.isFile(), "The project file does not exist or is invalid.");
    try {
      return Optional.of(parsePOM(new FileInputStream(pomFile)));
    } catch (FileNotFoundException e) {
      throw new RuntimeException("Could not load the project object model from file: " + pomFile.getAbsolutePath(), e);
    }
  }

  /**
   * Assumes that the passed input Stream contains content describing a POM and parses the content into a
   * {@link Document} for further manipulation.
   *
   * @param in the stream to be parsed. This stream will be closed after parsing the document.
   * @return the parsed document for further manipulation.
   */
  public static final Document parsePOM(InputStream in) {
    try {
      DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      Document document = documentBuilder.parse(in);
      return document;
    } catch (Exception e) {
      throw new RuntimeException("Could not load the project object model from input stream.", e);
    } finally {
      Closeables.closeQuietly(in);
    }
  }

  /**
   * Serializes the passed document which should contain POM content to the project file of the passed Maven project.
   *
   * @param document the document to be serialized.
   * @param project the project from which the serialization target will be retrieved.
   */
  public static final void writePOM(Document document, MavenProject project) {
    File pom = project.getFile();
    Preconditions.checkArgument(pom != null && pom.exists() && pom.isFile(),
        "The passed project does not contain a valid POM file reference.");

    try {
      writePOM(document, new FileOutputStream(pom), true);
    } catch (Throwable t) {
      throw new RuntimeException("Could not serialize the project object model of the following module: "
          + ProjectToString.INSTANCE.apply(project), t);
    }
  }

  /**
   * Serializes the passed document which should contain POM content to the passed output stream.
   *
   * @param document the document to be serialized.
   * @param out the output stream where the document shall be written to.
   * @param closeOut whether to close the output stream afterwards or not.
   */
  public static final void writePOM(Document document, OutputStream out, boolean closeOut) {
    try {
      Transformer transformer = TransformerFactory.newInstance().newTransformer();
      DOMSource source = new DOMSource(document);
      transformer.transform(source, new StreamResult(out));
      // append newline to end-of-file
      // is there any elegant way to do that on Document or in Transformer?
      out.write(LINE_SEPERATOR);
    } catch (Exception e) {
      throw new RuntimeException("Could not serialize the project object model to given output stream.", e);
    } finally {
      if (closeOut) {
        try {
          Closeables.close(out, true);
        } catch (IOException e) {
          throw new RuntimeException("Actually this should not happen :(", e);
        }
      }
    }
  }

  /**
   * Changes the project version of the POM as well as directly in the XML document preserving the whole document
   * formatting.
   *
   * @param model the POM where to adapt the project version.
   * @param document the POM as an XML document in which the project version shall be adapted.
   * @param newVersion the new project version to set.
   */
  public static void setProjectVersion(Model model, Document document, String newVersion) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    // if model version is null, the parent version is inherited
    if (model.getVersion() != null) {
      // first step: update the version of the in-memory project
      model.setVersion(newVersion);

      // second step: update the project version in the DOM document that is then serialized for later building
      NodeList children = document.getDocumentElement().getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        if (Objects.equal(child.getNodeName(), PomUtil.NODE_NAME_VERSION)) {
          child.setTextContent(newVersion);
        }
      }
    }
  }

  /**
   * Changes the project's parent version of the POM as well as directly in the XML document preserving the whole
   * document formatting.
   *
   * @param model the POM where to adapt the project's parent version.
   * @param document the POM as an XML document in which the project's parent version shall be adapted.
   * @param newParentVersion the new version to set for the project parent.
   */
  public static void setParentVersion(Model model, Document document, String newParentVersion) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    // first step: update parent version of the in-memory model
    Parent parent = model.getParent();
    if (parent != null) {
      parent.setVersion(newParentVersion);
    }

    // second step: update the parent version in the DOM document that will be serialized for later building
    Node parentNode = document.getDocumentElement().getElementsByTagName(PomUtil.NODE_NAME_PARENT).item(0);
    if (parentNode != null) {
      NodeList children = parentNode.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        if (Objects.equal(child.getNodeName(), PomUtil.NODE_NAME_VERSION)) {
          child.setTextContent(newParentVersion);
        }
      }
    }
  }

  /**
   * Changes the reactor dependency version of the POM as well as directly in the XML document preserving the whole
   * document formatting.
   *
   * @param dependency the reactor dependency where to adapt the project version.
   * @param document the POM as an XML document in which the project version shall be adapted.
   * @param dependenciesPath the XPath to the {@code dependencies} starting from {@code /project/}.
   *          e.g. {@code /profiles/profile[id[text()='release']]/dependencyManagement}.
   * @param newVersion the new dependency version to set.
   */
  public static void setDependencyVersion(Dependency dependency, Document document, String dependenciesPath,
      String newVersion) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    // first step: update dependency version of the in-memory model
    dependency.setVersion(newVersion);

    // second step: update the dependency version in the DOM document that is then serialized for later building
    NodeList dependencyNodes;
    try {
      List<String> xPathParts = new ArrayList<>();
      xPathParts.add(NODE_NAME_PROJECT);
      if (dependenciesPath.startsWith("/")) {
        dependenciesPath = dependenciesPath.substring("/".length());
      }
      if (!dependenciesPath.isEmpty()) {
        xPathParts.add(dependenciesPath);
      }
      xPathParts.add(NODE_NAME_DEPENDENCIES);
      xPathParts.add(NODE_NAME_DEPENDENCY + "[groupId[text()='" + dependency.getGroupId() + "']"
          + " and artifactId[text()='" + dependency.getArtifactId() + "']]");
      String expression = "/" + Joiner.on("/").skipNulls().join(xPathParts);

      XPath xPath = XPathFactory.newInstance().newXPath();
      dependencyNodes = (NodeList) xPath.evaluate(expression, document.getDocumentElement(), XPathConstants.NODESET);

    } catch (XPathExpressionException e) {
      String message = "Cannot evaluate xPath against '" + dependenciesPath + "'.";
      throw new RuntimeException(message, e);
    }

    for (int i = 0; i < dependencyNodes.getLength(); i++) {
      Node dependencyNode = dependencyNodes.item(i);
      NodeList childNodes = dependencyNode.getChildNodes();
      for (int j = 0; j < childNodes.getLength(); j++) {
        Node childNode = childNodes.item(j);
        if (Objects.equal(childNode.getNodeName(), PomUtil.NODE_NAME_VERSION)) {
          childNode.setTextContent(newVersion);
        }
      }
    }
  }

  /**
   * Queries for the project build node and creates one on demand.
   *
   * @param document the document from which to get the node or where to create the node at.
   * @param createOnDemand {@code true} if the build node shall be created when not present.
   * @return the build node of the document or {@code null} if the node doesn't exist and {@code createOnDemand} was set
   *         to {@code false}.
   */
  public static Node getOrCreateBuildNode(Document document, boolean createOnDemand) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    Node build = null;
    Node project = document.getElementsByTagName(NODE_NAME_PROJECT).item(0);
    NodeList children = project.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node node = children.item(i);
      if (Objects.equal(NODE_NAME_BUILD, node.getNodeName())) {
        build = node;
        break;
      }
    }
    if (build == null && createOnDemand) {
      build = document.createElement(NODE_NAME_BUILD);
      project.appendChild(build);
    }

    return build;
  }

  /**
   * Queries for the project build plugins node and creates one on demand. Creation is cascaded backwards.
   *
   * @param document the document from which to get the node or where to create the node at.
   * @param createOnDemand {@code true} if the plugins node and parents shall be created when not present.
   * @return the plugins node of the document or {@code null} if the node doesn't exist and {@code createOnDemand} was
   *         set to {@code false}.
   */
  public static Node getOrCreatePluginsNode(Document document, boolean createOnDemand) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    Node build = getOrCreateBuildNode(document, createOnDemand);
    Node plugins = null;
    if (build != null) {
      NodeList children = build.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node node = children.item(i);
        if (Objects.equal(NODE_NAME_PLUGINS, node.getNodeName())) {
          plugins = node;
          break;
        }
      }
      if (plugins == null && createOnDemand) {
        plugins = document.createElement(NODE_NAME_PLUGINS);
        build.appendChild(plugins);
      }
    }

    return plugins;
  }

  /**
   * Queries the document for a specific plugin node which is identified by its groupId and artifactId.
   *
   * @param document the document from which the plugin shall be retrieved if one is configured.
   * @param groupId the groupId of the searched plugin.
   * @param artifactId the artifactId of the searched plugin.
   * @return the queried plugin node or {@code null} if none is configured.
   */
  public static Node getPlugin(Document document, String groupId, String artifactId) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    Node pluginsNode = getOrCreatePluginsNode(document, false);
    if (pluginsNode != null) {
      NodeList plugins = pluginsNode.getChildNodes();
      for (int i = 0; i < plugins.getLength(); i++) {
        Node plugin = plugins.item(i);
        if (Objects.equal(NODE_NAME_PLUGIN, plugin.getNodeName())) {
          NodeList pluginSettings = plugin.getChildNodes();
          boolean gidMatches = false;
          boolean aidMatches = false;
          for (int j = 0; j < pluginSettings.getLength(); j++) {
            Node setting = pluginSettings.item(j);
            if (Objects.equal(NODE_NAME_GROUP_ID, setting.getNodeName())) {
              if (Objects.equal(groupId, setting.getTextContent())) {
                gidMatches = true;
              }
            } else if (Objects.equal(NODE_NAME_ARTIFACT_ID, setting.getNodeName())) {
              if (Objects.equal(artifactId, setting.getTextContent())) {
                aidMatches = true;
              }
            }
            if (gidMatches && aidMatches) {
              return plugin;
            }
          }
        }
      }
    }
    return null;
  }

  /**
   * Creates a plugin node in the given POM document or returns an already existing plugin node with the passed
   * coordinates.
   *
   * @param document the document where to create the plugin at.
   * @param groupId the groupId of the plugin to be created.
   * @param artifactId the artifactId of the plugin to be created.
   * @param version the version of the plugin to be created.
   * @return the node representing this plugin.
   */
  public static Node createPlugin(Document document, String groupId, String artifactId, String version) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    Node plugins = getOrCreatePluginsNode(document, true);

    Node existingPlugin = getPlugin(document, groupId, artifactId);
    if (existingPlugin != null) {
      if (!hasChildNode(existingPlugin, NODE_NAME_VERSION)) {
        Element ver = document.createElement(NODE_NAME_VERSION);
        ver.setTextContent(version);
        existingPlugin.appendChild(ver);
      }
      return existingPlugin;
    } else {
      Element plugin = document.createElement(NODE_NAME_PLUGIN);
      plugins.appendChild(plugin);

      Element gid = document.createElement(NODE_NAME_GROUP_ID);
      gid.setTextContent(groupId);
      plugin.appendChild(gid);

      Element aid = document.createElement(NODE_NAME_ARTIFACT_ID);
      aid.setTextContent(artifactId);
      plugin.appendChild(aid);

      Element ver = document.createElement(NODE_NAME_VERSION);
      ver.setTextContent(version);
      plugin.appendChild(ver);
      return plugin;
    }
  }

  /**
   * Creates a new execution element under the given plugin node.
   *
   * @param plugin the plugin under which the new execution shall be created.
   * @param id the execution id.
   * @param phase the phase in which the execution shall run.
   * @param goals the goals to be executed.
   * @return the freshly created execution element.
   */
  public static Node createPluginExecution(Node plugin, String id, Optional<String> phase, String... goals) {
    Document document = plugin.getOwnerDocument();

    Node executions = null;
    NodeList children = plugin.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node child = children.item(i);
      if (Objects.equal(NODE_NAME_EXECUTIONS, child.getNodeName())) {
        executions = child;
        break;
      }
    }
    if (executions == null) {
      executions = document.createElement(NODE_NAME_EXECUTIONS);
      plugin.appendChild(executions);
    }

    Element execution = document.createElement(NODE_NAME_EXECUTION);
    executions.appendChild(execution);

    Element idNode = document.createElement(NODE_NAME_ID);
    idNode.setTextContent(id);
    execution.appendChild(idNode);

    if (phase.isPresent()) {
      Element phaseNode = document.createElement(NODE_NAME_PHASE);
      phaseNode.setTextContent(phase.get());
      execution.appendChild(phaseNode);
    }

    if (goals.length > 0) {
      Element goalsNode = document.createElement(NODE_NAME_GOALS);
      execution.appendChild(goalsNode);

      for (String goal : goals) {
        Element goalNode = document.createElement(NODE_NAME_GOAL);
        goalNode.setTextContent(goal);
        goalsNode.appendChild(goalNode);
      }
    }

    return execution;
  }

  /**
   * Queries the document for an existing SCM node and creates one on demand if requested.
   *
   * @param document the document to query.
   * @param createOnDemand if {@code true} the node will be created on demand.
   * @return the SCM node of the document or {@code null} if the node doesn't exist and {@code createOnDemand} was set
   *         to {@code false}.
   */
  public static Node getOrCreateScmNode(Document document, boolean createOnDemand) {
    Preconditions.checkArgument(hasChildNode(document, NODE_NAME_PROJECT),
        "The document doesn't seem to be a POM model, project element is missing.");

    NodeList scmNodeList = document.getElementsByTagName(NODE_NAME_SCM);
    Node scm = null;
    if (scmNodeList.getLength() == 0 && createOnDemand) {
      scm = document.createElement(NODE_NAME_SCM);
      document.getDocumentElement().appendChild(scm);
    } else {
      scm = scmNodeList.item(0);
    }
    return scm;
  }

  /**
   * Sets the text content of the given node to the specified value preserving all whitespace.
   *
   * @param parentNode the parent node of the node where to set the text content.
   * @param nodeName the name of the node to set the text content at.
   * @param content the text content to set.
   * @param createOnDemand if {@code true} the node with name {@code nodeName} will be created as a child of
   *          {@code parentNode} if it does not exist.
   */
  public static void setNodeTextContent(Node parentNode, String nodeName, String content, boolean createOnDemand) {
    Node node = null;
    NodeList children = parentNode.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node n = children.item(i);
      if (Objects.equal(nodeName, n.getNodeName())) {
        node = n;
        break;
      }
    }

    if (node == null && createOnDemand) {
      node = parentNode.getOwnerDocument().createElement(nodeName);
      if (children.getLength() > 0) {
        Node lastChild = children.item(children.getLength() - 1);
        Text lineBreak = parentNode.getOwnerDocument().createTextNode("\n");
        parentNode.insertBefore(lineBreak, lastChild);
        parentNode.insertBefore(node, lastChild);
      } else {
        parentNode.appendChild(node);
      }
    }

    if (node != null) {
      node.setTextContent(content);
    }
  }

  /**
   * Deletes the specified node from the given parent.
   *
   * @param parentNode the parent of the node to delete.
   * @param nodeName the name of the node to delete.
   */
  public static void deleteNode(Node parentNode, String nodeName) {
    Node nodeToDelete = null;
    NodeList children = parentNode.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node n = children.item(i);
      if (Objects.equal(nodeName, n.getNodeName())) {
        nodeToDelete = n;
        break;
      }
    }

    if (nodeToDelete != null) {
      parentNode.removeChild(nodeToDelete);
    }
  }

  /**
   * Queries a node for a child with a specific name.
   *
   * @param parentNode the parent to query for the node.
   * @param nodeName the name of the searched node.
   * @return {@code true} if the parent contains a node with the specified name.
   */
  public static boolean hasChildNode(Node parentNode, String nodeName) {
    NodeList children = parentNode.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node n = children.item(i);
      if (Objects.equal(nodeName, n.getNodeName())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Get the text content of a specified child node (the first one) if it is present.
   *
   * @param parentNode the parent to query for the node.
   * @param nodeName the name of the child node from which the text content is requested.
   * @return the text content of the searched node.
   */
  public static Optional<String> getChildNodeTextContent(Node parentNode, String nodeName) {
    String value = null;

    NodeList children = parentNode.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node n = children.item(i);
      if (Objects.equal(nodeName, n.getNodeName())) {
        value = n.getTextContent();
        break;
      }
    }

    return Optional.fromNullable(value);
  }
} public enum VersionUpgradeStrategy {
  /**
   * The first part of the version (index 0).
   */
  MAJOR((short) 0),
  /**
   * The second part of the version (index 1).
   */
  MINOR((short) 1),
  /**
   * The third part of the version (index 2).
   */
  INCREMENTAL((short) 2),
  /**
   * The lowest possible (rightmost) part of the version (index -1).
   */
  DEFAULT((short) -1);

  private short versionSegmentIndex;

  private VersionUpgradeStrategy(short index) {
    this.versionSegmentIndex = index;
  }

  /**
   * @return the index of the version part.
   */
  public short getVersionSegmentIndex() {
    return this.versionSegmentIndex;
  }
} public enum ProjectToCoordinates implements Function<MavenProject, ArtifactCoordinates> {
  /**
   * Uses all relevant properties of the project.
   */
  INSTANCE(true, null),
  /**
   * Assumes the default empty version for the project.
   */
  EMPTY_VERSION(false, null),
  /**
   * Uses the relevant properties of the project but assumes "pom" packaging.
   */
  POM(true, PomUtil.ARTIFACT_TYPE_POM),
  /**
   * Assumes an empty project version and packaging type "pom".
   */
  EMPTY_VERSION_POM(false, PomUtil.ARTIFACT_TYPE_POM);

  private boolean includeVersion;
  private String type;

  private ProjectToCoordinates(boolean version, String type) {
    this.includeVersion = version;
    this.type = type;
  }

  @Override
  public ArtifactCoordinates apply(MavenProject p) {
    if (this.includeVersion) {
      return new ArtifactCoordinates(p.getGroupId(), p.getArtifactId(), p.getVersion(),
          this.type != null ? this.type : p.getPackaging());
    } else {
      return new ArtifactCoordinates(p.getGroupId(), p.getArtifactId(), MavenProject.EMPTY_PROJECT_VERSION,
          this.type != null ? this.type : p.getPackaging());
    }
  }
} public enum ProjectToString implements Function<MavenProject, String> {
  /**
   * The default instance which prints the groupId, artifactId and the version.
   */
  INSTANCE(false, true),
  /**
   * An instance which uses the groupId, the artifactId, the version and the packaging of the artifact.
   */
  INCLUDE_PACKAGING(true, true),
  /**
   * An instance which only prints the groupId and the artifactId.
   */
  EXCLUDE_VERSION(false, false);

  private boolean includePackaging;
  private boolean includeVersion;

  private ProjectToString(boolean includePackaging, boolean includeVersion) {
    this.includePackaging = includePackaging;
    this.includeVersion = includeVersion;
  }

  @Override
  public String apply(MavenProject p) {
    StringBuilder sb = new StringBuilder(p.getGroupId());
    sb.append(":").append(p.getArtifactId());
    if (this.includePackaging && p.getPackaging() != null) {
      sb.append(":").append(p.getPackaging());
    }
    if (this.includeVersion && p.getVersion() != null) {
      sb.append(":").append(p.getVersion());
    }
    return sb.toString();
  }
} public class ScmPomVersionsMergeClient implements MergeClient {

  @Override
  public void merge(InputStream local, InputStream remote, InputStream base, OutputStream result) throws ScmException {
    Optional<Model> localModel = loadModel(local);
    if (!localModel.isPresent()) {
      // TODO implement merge of other files!
      throw new ScmException(ScmOperation.MERGE, "Unable to merge non-POM changes.");
    }

    byte[] remoteData = null;
    try {
      remoteData = ByteStreams.toByteArray(remote);
    } catch (IOException e) {
      throw new ScmException(ScmOperation.MERGE, "Unable to read remote content!", e);
    } finally {
      Closeables.closeQuietly(remote);
    }

    Optional<Model> remoteModel = loadModel(new ByteArrayInputStream(remoteData));
    Optional<Model> baseModel = loadModel(base);

    Model resultModel = loadModel(new ByteArrayInputStream(remoteData)).get();
    mergeVersions(localModel.get(), remoteModel.get(), baseModel.get(), resultModel);
    mergeParentVersions(localModel.get(), remoteModel.get(), baseModel.get(), resultModel);

    try {
      Document document = PomUtil.parsePOM(new ByteArrayInputStream(remoteData));
      PomUtil.setProjectVersion(resultModel, document, resultModel.getVersion());
      if (resultModel.getParent() != null) {
        PomUtil.setParentVersion(resultModel, document, resultModel.getParent().getVersion());
      }
      PomUtil.writePOM(document, result, false);
    } catch (Throwable t) {
      throw new ScmException(ScmOperation.MERGE, "Could not serialize merged POM!", t);
    }
  }

  private void mergeVersions(Model local, Model remote, Model base, Model result) throws ScmException {
    boolean remoteChange = !Objects.equal(remote.getVersion(), base.getVersion());
    if (remoteChange) {
      if (!Objects.equal(local.getVersion(), remote.getVersion())) {
        if (MavenVersionUtil.isNewerVersion(local.getVersion(), remote.getVersion())) {
          result.setVersion(local.getVersion());
        } else {
          result.setVersion(remote.getVersion());
          // throw new ScmException(ScmOperation.MERGE,
          // "Could not merge POM version changes since the new remote version is higher than the local one!");
        }
      }
    } else {
      result.setVersion(local.getVersion());
    }
  }

  private void mergeParentVersions(Model local, Model remote, Model base, Model result) throws ScmException {
    String localParentVersion = local.getParent() != null ? local.getParent().getVersion() : null;
    String remoteParentVersion = remote.getParent() != null ? remote.getParent().getVersion() : null;
    String baseParentVersion = base.getParent() != null ? base.getParent().getVersion() : null;

    boolean remoteParentRemoved = remoteParentVersion == null && baseParentVersion != null;
    boolean remoteParentAdded = remoteParentVersion != null && baseParentVersion == null;
    boolean remoteParentVersionChanged = !Objects.equal(remoteParentVersion, baseParentVersion);

    boolean localParentRemoved = localParentVersion == null && baseParentVersion != null;
    boolean localParentAdded = localParentVersion != null && baseParentVersion == null;
    boolean localParentVersionChanged = !Objects.equal(localParentVersion, baseParentVersion);

    if (localParentAdded) {
      // if locally added the base had no parent (remote remove and change is not relevant)
      if (remoteParentAdded) {
        if (Objects.equal(local.getParent().getArtifactId(), remote.getParent().getArtifactId())
            && Objects.equal(local.getParent().getGroupId(), remote.getParent().getGroupId())) {
          if (MavenVersionUtil.isNewerVersion(local.getParent().getVersion(), remote.getParent().getVersion())) {
            result.setParent(local.getParent());
          }
        } else {
          throw new ScmException(ScmOperation.MERGE,
              "Could not merge local and remote POM parent changes since both versions added different parent artifacts.");
        }
      } else {
        result.setParent(local.getParent());
      }
    } else if (localParentRemoved) {
      // if locally removed the base had a parent (remote add is not relevant and remote remove is ok)
      if (remoteParentVersionChanged) {
        throw new ScmException(ScmOperation.MERGE,
            "Could not merge POM parent version conflicts since in the local POM the parent had been removed and in the remote POM the parent had been changed.");
      } else {
        result.getParent().setVersion(localParentVersion);
      }
    } else if (localParentVersionChanged) {
      // if locally changed the base had a parent (remote add is not relevant)
      if (remoteParentVersionChanged) {
        if (Objects.equal(local.getParent().getArtifactId(), remote.getParent().getArtifactId())
            && Objects.equal(local.getParent().getGroupId(), remote.getParent().getGroupId())) {
          if (MavenVersionUtil.isNewerVersion(local.getParent().getVersion(), remote.getParent().getVersion())) {
            result.setParent(local.getParent());
          }
        } else {
          throw new ScmException(ScmOperation.MERGE,
              "Could not merge local and remote POM parent changes since both versions are referencing different parent artifacts.");
        }
      } else if (remoteParentRemoved) {
        throw new ScmException(ScmOperation.MERGE,
            "Could not merge POM parent version conflicts since in the local POM the parent had been updated while in the remote POM the parent had been removed.");
      } else {
        result.getParent().setVersion(localParentVersion);
      }
    }
  }

  private Optional<Model> loadModel(InputStream in) {
    MavenXpp3Reader reader = new MavenXpp3Reader();
    try {
      Model model = reader.read(in);
      return Optional.of(model);
    } catch (Exception e) {
      return Optional.absent();
    } finally {
      Closeables.closeQuietly(in);
    }
  }
} @Singleton
public class ScmProviderRegistry {
  @Inject
  private com.itemis.maven.plugins.cdi.logging.Logger log;
  @Inject
  @Any
  private Instance<ScmProvider> providers;
  @Inject
  private MavenProject project;
  @Inject
  @Named("scmUsername")
  private String scmUsername;
  @Inject
  @Named("scmPassword")
  private String scmPassword;
  @Inject
  @Named("scmSshPassphrase")
  private String scmSshPassphrase;
  @Inject
  @Named("scmUsernameEnvVar")
  private String scmUsernameEnvVar;
  @Inject
  @Named("scmPasswordEnvVar")
  private String scmPasswordEnvVar;
  @Inject
  @Named("scmSshPassphraseEnvVar")
  private String scmSshPassphraseEnvVar;
  @Inject
  @Named("scmSshPrivateKeyEnvVar")
  private String scmSshPrivateKeyEnvVar;
  private String scmProviderName;
  private ScmProvider provider;

  private ScmProviderRegistry() {
  }

  @PostConstruct
  private void init() {
    Optional<String> providerName = MavenScmUtil.calcProviderName(this.project);
    if (!providerName.isPresent()) {
      this.log.error(
          "Could not determine SCM provider name from your POM configuration! Please check the SCM section of your POM and provide connections in the correct format (see also: https://maven.apache.org/scm/scm-url-format.html).");
    } else {
      this.log.debug("Resolved required SCM provider implementation to '" + providerName.get() + "'");
    }
    this.scmProviderName = providerName.orNull();
  }

  public ScmProvider getProvider() throws IllegalStateException {
    if (this.provider == null) {
      try {
        this.provider = this.providers.select(new ScmProviderTypeLiteral(this.scmProviderName)).get();
        checkProviderAPI();

        DefaultScmProviderInitialization initialization = new DefaultScmProviderInitialization(
            this.project.getBasedir());
        initialization.setLogger(new JavaLoggerAdapter(this.provider.getClass().getName(), this.log));
        initialization.setUsername(getScmUsername());
        initialization.setUsername(getScmUsername());
        initialization.setPassword(getScmPassword());
        initialization.setSshPrivateKey(getPrivateKey());
        initialization.setSshPrivateKeyPassphrase(getScmSshPassphrase());
        this.provider.initialize(initialization);
      } catch (IllegalStateException e) {
        throw e;
      } catch (Throwable t) {
        throw new IllegalStateException("No SCM provider found for SCM with name " + this.scmProviderName
            + ". Maybe you need to add an appropriate provider implementation as a dependency to the plugin.", t);
      }
    }
    return this.provider;
  }

  private void checkProviderAPI() throws IllegalStateException {
    boolean isIncompatible = false;
    Throwable cause = null;

    // compares all API methods against all implementation methods and fails on missing and/or wrong method signatures.
    for (Method apiMethod : ScmProvider.class.getDeclaredMethods()) {
      try {
        Method implMethod = this.provider.getClass().getDeclaredMethod(apiMethod.getName(),
            apiMethod.getParameterTypes());
        if (!Objects.equal(implMethod.getReturnType(), apiMethod.getReturnType())) {
          isIncompatible = true;
          break;
        }
      } catch (Throwable e) {
        isIncompatible = true;
        cause = e;
        break;
      }
    }

    if (isIncompatible) {
      this.log.error(
          "The SCM provider API and the configured implementation for SCMs with identifier '" + this.scmProviderName
              + "' are incompatible. Please check the compatibility notes of the chosen provider implementation.");
      if (cause != null) {
        throw new IllegalStateException(
            "Invalid SCM provider API version of provider implementation '" + this.scmProviderName + "'.", cause);
      } else {
        throw new IllegalStateException(
            "Invalid SCM provider API version of provider implementation '" + this.scmProviderName + "'.");
      }
    }
  }

  private String getScmUsername() {
    String username = Strings.emptyToNull(this.scmUsername);
    if (username == null && StringUtils.isNotBlank(this.scmUsernameEnvVar)) {
      username = Strings.emptyToNull(System.getenv(this.scmUsernameEnvVar));
    }
    return username;
  }

  private String getScmPassword() {
    String password = Strings.emptyToNull(this.scmPassword);
    if (password == null && StringUtils.isNotBlank(this.scmPasswordEnvVar)) {
      password = Strings.emptyToNull(System.getenv(this.scmPasswordEnvVar));
    }
    return password;
  }

  private String getScmSshPassphrase() {
    String passphrase = Strings.emptyToNull(this.scmSshPassphrase);
    if (passphrase == null && StringUtils.isNotBlank(this.scmSshPassphraseEnvVar)) {
      passphrase = Strings.emptyToNull(System.getenv(this.scmSshPassphraseEnvVar));
    }
    return passphrase;
  }

  private String getPrivateKey() {
    String privateKey = null;
    if (StringUtils.isNotBlank(this.scmSshPrivateKeyEnvVar)) {
      return Strings.emptyToNull(System.getenv(this.scmSshPrivateKeyEnvVar));
    }
    return null;
  }

  @PreDestroy
  private void disposeProvider() {
    if (this.provider != null) {
      this.provider.close();
      this.provider = null;
    }
  }
}
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.w3c.dom.Document;
import com.google.common.collect.Maps;
import com.itemis.maven.aether.ArtifactCoordinates;
import com.itemis.maven.plugins.cdi.ExecutionContext;
import com.itemis.maven.plugins.cdi.annotations.ProcessingStep;
import com.itemis.maven.plugins.cdi.annotations.RollbackOnError;
import com.itemis.maven.plugins.unleash.ReleasePhase;
import com.itemis.maven.plugins.unleash.scm.ScmProvider;
import com.itemis.maven.plugins.unleash.scm.requests.RevertCommitsRequest;
import com.itemis.maven.plugins.unleash.util.DevVersionUtil;
import com.itemis.maven.plugins.unleash.util.PomUtil;
import com.itemis.maven.plugins.unleash.util.VersionUpgradeStrategy;
import com.itemis.maven.plugins.unleash.util.functions.ProjectToCoordinates;
import com.itemis.maven.plugins.unleash.util.functions.ProjectToString;
import com.itemis.maven.plugins.unleash.util.scm.ScmPomVersionsMergeClient;
import com.itemis.maven.plugins.unleash.util.scm.ScmProviderRegistry;
package com.itemis.maven.plugins.unleash.steps.actions;




/**
 * Updates the POMs of all modules of the project with the new development version, reverts all SCM changes so that the
 * SCM paths match the current branch again and commits the changes finally.
 *
 * @author <a href="mailto:stanley.hillner@itemis.de">Stanley Hillner</a>
 * @since 1.0.0
 */
@ProcessingStep(id = "setDevVersion", description = "Updates the projects with the next development versions, reverts previous SCM path changes and finally commits the changes to the current branch.", requiresOnline = true)
public class SetNextDevVersion extends AbstractVersionsStep {
  @Inject
  MavenProject project;
  @Inject
  private ScmProviderRegistry scmProviderRegistry;
  @Inject
  @Named("scmMessagePrefix")
  private String scmMessagePrefix;
  @Inject
  private DevVersionUtil util;
  @Inject
  private VersionUpgradeStrategy versionUpgradeStrategy;
  private ScmProvider scmProvider;

  @Override
  public void execute(ExecutionContext context) throws MojoExecutionException, MojoFailureException {
    this.log.info("Preparing project modules for next development cycle.");

    this.scmProvider = this.scmProviderRegistry.getProvider();
    this.cachedPOMs = Maps.newHashMap();

    for (MavenProject project : this.reactorProjects) {
      this.log.debug("\tPreparing module '" + ProjectToString.INSTANCE.apply(project) + "'.");