diff --git a/tcwebhooks-core/src/main/java/webhook/teamcity/settings/WebHookConfig.java b/tcwebhooks-core/src/main/java/webhook/teamcity/settings/WebHookConfig.java index 9f1d9c43..3eb82cd9 100644 --- a/tcwebhooks-core/src/main/java/webhook/teamcity/settings/WebHookConfig.java +++ b/tcwebhooks-core/src/main/java/webhook/teamcity/settings/WebHookConfig.java @@ -1,749 +1,761 @@ -package webhook.teamcity.settings; - -import static webhook.teamcity.BuildStateEnum.BEFORE_BUILD_FINISHED; -import static webhook.teamcity.BuildStateEnum.BUILD_BROKEN; -import static webhook.teamcity.BuildStateEnum.BUILD_FAILED; -import static webhook.teamcity.BuildStateEnum.BUILD_FINISHED; -import static webhook.teamcity.BuildStateEnum.BUILD_FIXED; -import static webhook.teamcity.BuildStateEnum.BUILD_INTERRUPTED; -import static webhook.teamcity.BuildStateEnum.CHANGES_LOADED; -import static webhook.teamcity.BuildStateEnum.BUILD_STARTED; -import static webhook.teamcity.BuildStateEnum.BUILD_SUCCESSFUL; -import static webhook.teamcity.BuildStateEnum.RESPONSIBILITY_CHANGED; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; - -import jetbrains.buildServer.log.Loggers; -import jetbrains.buildServer.serverSide.SBuildType; -import lombok.AllArgsConstructor; -import lombok.Builder; - -import org.jdom.DataConversionException; -import org.jdom.Element; - -import webhook.teamcity.BuildState; -import webhook.teamcity.BuildStateEnum; -import webhook.teamcity.TeamCityIdResolver; -import webhook.teamcity.auth.WebHookAuthConfig; -import webhook.teamcity.payload.WebHookPayloadDefaultTemplates; -import webhook.teamcity.settings.converter.WebHookBuildStateConverter; - -@Builder @AllArgsConstructor -public class WebHookConfig { - private static final String EL_TRIGGER_FILTERS = "trigger-filters"; - private static final String EL_HEADERS = "headers"; - private static final String ATTR_PREEMPTIVE = "preemptive"; - private static final String CHECKED = "checked "; - private static final String EL_CUSTOM_TEMPLATE = "custom-template"; - private static final String EL_CUSTOM_TEMPLATES = "custom-templates"; - private static final String ATTR_VALUE = "value"; - private static final String ATTR_NAME = "name"; - private static final String ATTR_PARAM = "param"; - private static final String EL_PARAMETERS = "parameters"; - private static final String ATTR_STATEMASK = "statemask"; - private static final String ATTR_TYPE = "type"; - private static final String EL_STATE = "state"; - private static final String ATTR_TEMPLATE = "template"; - private static final String ATTR_FORMAT = "format"; - private static final String ATTR_ENABLED = "enabled"; - private static final String EL_STATES = "states"; - private static final String EL_BUILD_TYPES = "build-types"; - private static final String ATTR_ENABLED_FOR_ALL = "enabled-for-all"; - private static final String ATTR_ENABLED_FOR_SUBPROJECTS = "enabled-for-subprojects"; - private static final String LOG_PREFIX_WEB_HOOK_CONFIG = "WebHookConfig :: "; - private SortedMap extraParameters; - @Builder.Default private Boolean enabled = true; - @Builder.Default private String uniqueKey = ""; - private String url; - @Builder.Default private String payloadFormat = null; - @Builder.Default private String payloadTemplate = "none"; - @Builder.Default private BuildState states = new BuildState(); - private SortedMap templates; // This defaults to null so that it's not in the XML if empty. - @Builder.Default private Boolean allBuildTypesEnabled = true; - @Builder.Default private Boolean subProjectsEnabled = true; - @Builder.Default private Set enabledBuildTypesSet = new HashSet<>(); - @Builder.Default private String authType = ""; - @Builder.Default private Boolean authEnabled = false; - @Builder.Default private Map authParameters = new LinkedHashMap<>(); - @Builder.Default private Boolean authPreemptive = true; - private List filters; - private List headers; - @Builder.Default private String projectInternalId = null; - @Builder.Default private String projectExternalId = null; - - @SuppressWarnings("unchecked") - public WebHookConfig (Element e) { - - this.uniqueKey = "id_" + getRandomKey(); - this.extraParameters = new TreeMap<>(); - this.states = new BuildState(); - this.templates = new TreeMap<>(); - this.allBuildTypesEnabled = true; - this.subProjectsEnabled = true; - this.enabledBuildTypesSet = new HashSet<>(); - this.authType = ""; - this.authEnabled = false; - this.authParameters = new LinkedHashMap<>(); - this.authPreemptive = true; - this.filters = new ArrayList<>(); - this.headers = new ArrayList<>(); - - if (e.getAttribute("url") != null){ - this.setUrl(e.getAttributeValue("url")); - } - - if (e.getAttribute(ATTR_ENABLED) != null){ - this.setEnabled(Boolean.parseBoolean(e.getAttributeValue(ATTR_ENABLED))); - } - - if (e.getAttribute(ATTR_STATEMASK) != null){ - this.setBuildStates(WebHookBuildStateConverter.convert(Integer.parseInt(e.getAttributeValue(ATTR_STATEMASK)))); - } - - if (e.getAttribute("key") != null){ - this.setUniqueKey(e.getAttributeValue("key")); - } - - if (e.getAttribute(ATTR_FORMAT) != null){ - this.setPayloadFormat(e.getAttributeValue(ATTR_FORMAT)); - } else { - // Set to nvpairs by default for backward compatibility. - this.setPayloadFormat("nvpairs"); - } - - if (e.getAttribute(ATTR_TEMPLATE) != null){ - this.setPayloadTemplate(e.getAttributeValue(ATTR_TEMPLATE)); - } - - if(e.getChild(EL_STATES) != null){ - Element eStates = e.getChild(EL_STATES); - List statesList = eStates.getChildren(EL_STATE); - if ( ! statesList.isEmpty()){ - for(Element eState : statesList) - { - try { - states.setEnabled(BuildStateEnum.findBuildState(eState.getAttributeValue(ATTR_TYPE)), - eState.getAttribute(ATTR_ENABLED).getBooleanValue()); - } catch (DataConversionException e1) { - Loggers.SERVER.warn(LOG_PREFIX_WEB_HOOK_CONFIG + e1.getMessage()); - } - } - } - } - - if(e.getChild(EL_BUILD_TYPES) != null){ - Element eTypes = e.getChild(EL_BUILD_TYPES); - if (eTypes.getAttribute(ATTR_ENABLED_FOR_ALL) != null){ - try { - this.enableForAllBuildsInProject(eTypes.getAttribute(ATTR_ENABLED_FOR_ALL).getBooleanValue()); - } catch (DataConversionException e1) { - Loggers.SERVER.warn(LOG_PREFIX_WEB_HOOK_CONFIG + e1.getMessage()); - } - } - if (eTypes.getAttribute(ATTR_ENABLED_FOR_SUBPROJECTS) != null){ - try { - this.enableForSubProjects(eTypes.getAttribute(ATTR_ENABLED_FOR_SUBPROJECTS).getBooleanValue()); - } catch (DataConversionException e1) { - Loggers.SERVER.warn(LOG_PREFIX_WEB_HOOK_CONFIG + e1.getMessage()); - } - } - if (!isEnabledForAllBuildsInProject()){ - List typesList = eTypes.getChildren("build-type"); - if ( ! typesList.isEmpty()){ - for(Element eType : typesList) - { - if (eType.getAttributeValue("id")!= null){ - enabledBuildTypesSet.add(eType.getAttributeValue("id")); - } - } - } - } - } - - if(e.getChild(EL_PARAMETERS) != null){ - Element eParams = e.getChild(EL_PARAMETERS); - List paramsList = eParams.getChildren(ATTR_PARAM); - if ( ! paramsList.isEmpty()){ - for(Element eParam : paramsList) - { - this.extraParameters.put( - eParam.getAttributeValue(ATTR_NAME), - eParam.getAttributeValue(ATTR_VALUE) - ); - } - } - } - - if(e.getChild(EL_CUSTOM_TEMPLATES) != null){ - Element eParams = e.getChild(EL_CUSTOM_TEMPLATES); - List templateList = eParams.getChildren(EL_CUSTOM_TEMPLATE); - if ( ! templateList.isEmpty()){ - for(Element eParam : templateList) - { - this.templates.put( - eParam.getAttributeValue(CustomMessageTemplate.TYPE), - CustomMessageTemplate.create( - eParam.getAttributeValue(CustomMessageTemplate.TYPE), - eParam.getAttributeValue(CustomMessageTemplate.TEMPLATE), - Boolean.parseBoolean(eParam.getAttributeValue(CustomMessageTemplate.ENABLED)) - ) - ); - } - } - } - - if(e.getChild("auth") != null){ - Element eAuth = e.getChild("auth"); - if (eAuth.getAttribute(ATTR_TYPE) != null){ - // We have an "auth" element - // Try to get the enabled flag - authType = eAuth.getAttribute(ATTR_TYPE).getValue(); - try { - authEnabled = eAuth.getAttribute(ATTR_ENABLED).getBooleanValue(); - } catch (DataConversionException e1){ - // And if it can't be read as boolean default it - // to true anyway (since we have the auth type). - authEnabled = true; - } - try { - if (eAuth.getAttribute(ATTR_PREEMPTIVE) != null){ - authPreemptive = eAuth.getAttribute(ATTR_PREEMPTIVE).getBooleanValue(); - } - } catch (DataConversionException e1){ - // And if it can't be read as boolean default it - // to true (which means creds are always sent). - authPreemptive = true; - } - Element eParams = eAuth.getChild("auth-parameters"); - if (eParams != null){ - List paramsList = eParams.getChildren(ATTR_PARAM); - if (!paramsList.isEmpty()){ - for(Element eParam : paramsList) - { - this.authParameters.put( - eParam.getAttributeValue(ATTR_NAME), - eParam.getAttributeValue(ATTR_VALUE) - ); - } - } - } - } - - } - - /* - - - - */ - if(e.getChild(EL_TRIGGER_FILTERS) != null){ - Element eParams = e.getChild(EL_TRIGGER_FILTERS); - List filterList = eParams.getChildren("filter"); - if (! filterList.isEmpty()){ - for(Element eParam : filterList) - { - this.filters.add( - - WebHookFilterConfig.create( - eParam.getAttributeValue(WebHookFilterConfig.VALUE), - eParam.getAttributeValue(WebHookFilterConfig.REGEX), - Boolean.parseBoolean(eParam.getAttributeValue(WebHookFilterConfig.ENABLED)) - ) - ); - } - } - } - - /* - -
- - */ - if(e.getChild(EL_HEADERS) != null){ - Element eParams = e.getChild(EL_HEADERS); - List headerList = eParams.getChildren(WebHookHeaderConfig.XML_ELEMENT_NAME); - if (! headerList.isEmpty()){ - for(Element eParam : headerList) - { - this.headers.add( - - WebHookHeaderConfig.create( - eParam.getAttributeValue(WebHookHeaderConfig.NAME), - eParam.getAttributeValue(WebHookHeaderConfig.VALUE) - ) - ); - } - } - } - - } - - private String getRandomKey() { - int min = 1000000; - int max = 1000000000; - Integer rand = min + new Random().nextInt((max - min) + 1); - return rand.toString(); - } - - /** - * WebHooksConfig constructor. Unchecked version. Use with caution!! - * This constructor does not check if the payloadFormat is valid. - * It will still allow you to add the format, but the webhook might not - * fire at runtime if the payloadFormat configured is not available. - * - * @param url - * @param enabled - * @param stateMask - * @param payloadFormat (unvalidated) - * @param webHookAuthConfig - */ - public WebHookConfig (String projectInternalId, String projectExternalId, String url, Boolean enabled, BuildState states, String payloadFormat, String payloadTemplate, boolean buildTypeAllEnabled, boolean buildTypeSubProjects, Set enabledBuildTypes, WebHookAuthConfig webHookAuthConfig){ - this.uniqueKey = "id_" + getRandomKey(); - this.setProjectInternalId(projectInternalId); - this.setProjectExternalId(projectExternalId); - this.extraParameters = new TreeMap<>(); - this.templates = new TreeMap<>(); - this.authType = ""; - this.authEnabled = false; - this.authParameters = new LinkedHashMap<>(); - this.authPreemptive = true; - this.filters = new ArrayList<>(); - this.headers = new ArrayList<>(); - this.setUrl(url); - this.setEnabled(enabled); - this.setBuildStates(states); - this.setPayloadFormat(payloadFormat); - this.setPayloadTemplate(payloadTemplate); - this.subProjectsEnabled = buildTypeSubProjects; - this.allBuildTypesEnabled = buildTypeAllEnabled; - if (!this.allBuildTypesEnabled){ - this.enabledBuildTypesSet = enabledBuildTypes; - } else { - this.enabledBuildTypesSet = new HashSet<>(); - } - if (webHookAuthConfig != null){ - this.authType = webHookAuthConfig.getType(); - this.authPreemptive = webHookAuthConfig.getPreemptive(); - this.authEnabled = true; - this.authParameters.putAll(webHookAuthConfig.getParameters()); - } - } - - private Element getKeyAndValueAsElement(Map map, String key, String elementName){ - Element e = new Element(elementName); - if (map.containsKey(key)){ - e.setAttribute(ATTR_NAME, key); - e.setAttribute(ATTR_VALUE,map.get(key)); - } - return e; - } - - public Element getAsElement(){ - Element el = new Element("webhook"); - el.setAttribute("url", this.getUrl()); - el.setAttribute(ATTR_ENABLED, String.valueOf(this.enabled)); - el.setAttribute(ATTR_FORMAT, String.valueOf(this.payloadFormat).toLowerCase()); - el.setAttribute(ATTR_TEMPLATE, String.valueOf(this.payloadTemplate)); - - Element statesEl = new Element(EL_STATES); - for (BuildStateEnum state : states.getStateSet()){ - Element e = new Element(EL_STATE); - e.setAttribute(ATTR_TYPE, state.getShortName()); - e.setAttribute(ATTR_ENABLED, Boolean.toString(states.enabled(state))); - statesEl.addContent(e); - } - el.addContent(statesEl); - - Element buildsEl = new Element(EL_BUILD_TYPES); - buildsEl.setAttribute(ATTR_ENABLED_FOR_ALL, Boolean.toString(isEnabledForAllBuildsInProject())); - buildsEl.setAttribute(ATTR_ENABLED_FOR_SUBPROJECTS, Boolean.toString(isEnabledForSubProjects())); - - for (String i : enabledBuildTypesSet){ - Element e = new Element("build-type"); - e.setAttribute("id", i); - buildsEl.addContent(e); - } - el.addContent(buildsEl); - - if (this.filters != null && ! this.filters.isEmpty()){ - Element filtersEl = new Element(EL_TRIGGER_FILTERS); - for (WebHookFilterConfig f : this.filters){ - filtersEl.addContent(f.getAsElement()); - } - el.addContent(filtersEl); - } - - if (this.extraParameters.size() > 0){ - Element paramsEl = new Element(EL_PARAMETERS); - for (String i : this.extraParameters.keySet()){ - paramsEl.addContent(this.getKeyAndValueAsElement(this.extraParameters, i, ATTR_PARAM)); - } - el.addContent(paramsEl); - } - - if (this.templates.size() > 0){ - Element templatesEl = new Element(EL_CUSTOM_TEMPLATES); - for (CustomMessageTemplate t : this.templates.values()){ - templatesEl.addContent(t.getAsElement()); - } - el.addContent(templatesEl); - } - - if (this.authType != null && ! this.authType.isEmpty()){ - Element authEl = new Element("auth"); - authEl.setAttribute(ATTR_ENABLED, this.authEnabled.toString()); - authEl.setAttribute(ATTR_TYPE, this.authType); - authEl.setAttribute(ATTR_PREEMPTIVE, this.authPreemptive.toString() ); - if (this.authParameters != null && ! this.authParameters.isEmpty()){ - Element paramsEl = new Element("auth-parameters"); - for (String i : this.authParameters.keySet()){ - paramsEl.addContent(this.getKeyAndValueAsElement(this.authParameters, i, ATTR_PARAM)); - } - authEl.addContent(paramsEl); - } - el.addContent(authEl); - } - - if (this.headers != null && ! this.headers.isEmpty()){ - Element headersEl = new Element(EL_HEADERS); - for (WebHookHeaderConfig h : this.headers){ - headersEl.addContent(h.getAsElement()); - } - el.addContent(headersEl); - } - - return el; - } - - // Getters and Setters.. - - public SortedMap getParams() { - return extraParameters; - } - - public boolean isEnabledForBuildType(SBuildType sBuildType){ - // If allBuildTypes enabled, return true, otherwise return whether the build is in the list of enabled buildTypes. - return isEnabledForAllBuildsInProject() ? true : enabledBuildTypesSet.contains(TeamCityIdResolver.getInternalBuildId(sBuildType)); - } - - public boolean isSpecificBuildTypeEnabled(SBuildType sBuildType){ - // Just check if this build type is only enabled for a specific build. - return enabledBuildTypesSet.contains(TeamCityIdResolver.getInternalBuildId(sBuildType)); - } - - public String getBuildTypeCountAsFriendlyString(){ - if (this.allBuildTypesEnabled && this.subProjectsEnabled){ - return "All builds & Sub-Projects"; - } else if (this.allBuildTypesEnabled){ // this.subProjectsEnabled is false - return "All builds"; - } else { - String subProjectsString = ""; - if (this.subProjectsEnabled){ - subProjectsString = " & All Sub-Project builds"; - } - int enabledBuildTypeCount = this.enabledBuildTypesSet.size(); - if (enabledBuildTypeCount == 1){ - return enabledBuildTypeCount + " build" + subProjectsString; - } - return enabledBuildTypeCount + " builds" + subProjectsString; - } - } - - public Boolean getEnabled() { - return enabled; - } - - public void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - - public BuildState getBuildStates() { - return states; - } - - public void setBuildStates(BuildState states) { - this.states = states; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getUniqueKey() { - return uniqueKey; - } - - public void setUniqueKey(String uniqueKey) { - this.uniqueKey = uniqueKey; - } - - public String getEnabledListAsString(){ - if (!this.enabled){ - return "Disabled"; - } else if (states.allEnabled()){ - return "All Build Events"; - } else if (states.noneEnabled()) { - return "None"; - } else { - String enabledStates = ""; - if (states.enabled(BuildStateEnum.BUILD_STARTED)){ - enabledStates += ", Build Started"; - } - if (states.enabled(BuildStateEnum.CHANGES_LOADED)){ - enabledStates += ", Changes Loaded"; - } - if (states.enabled(BuildStateEnum.BUILD_INTERRUPTED)){ - enabledStates += ", Build Interrupted"; - } - if (states.enabled(BuildStateEnum.BEFORE_BUILD_FINISHED)){ - enabledStates += ", Build Almost Completed"; - } - if (states.enabled(BuildStateEnum.RESPONSIBILITY_CHANGED)){ - enabledStates += ", Build Responsibility Changed"; - } - if (states.enabled(BuildStateEnum.BUILD_FAILED)){ - if (states.enabled(BuildStateEnum.BUILD_BROKEN)){ - enabledStates += ", Build Broken"; - } else { - enabledStates += ", Build Failed"; - } - } - if (states.enabled(BuildStateEnum.BUILD_SUCCESSFUL)){ - if (states.enabled(BuildStateEnum.BUILD_FIXED)){ - enabledStates += ", Build Fixed"; - } else { - enabledStates += ", Build Successful"; - } - } - if (enabledStates.length() > 0){ - return enabledStates.substring(1); - } else { - return "None"; - } - } - } - - public String getWebHookEnabledAsChecked() { - if (this.enabled){ - return CHECKED; - } - return ""; - } - - public String getStateAllAsChecked() { - if (states.allEnabled()){ - return CHECKED; - } - return ""; - } - - public String getStateBuildStartedAsChecked() { - if (states.enabled(BUILD_STARTED)){ - return CHECKED; - } - return ""; - } - - public String getStateChangesLoadedAsChecked() { - if (states.enabled(CHANGES_LOADED)){ - return CHECKED; - } - return ""; - } - - public String getStateBuildFinishedAsChecked() { - if (states.enabled(BUILD_FINISHED)){ - return CHECKED; - } - return ""; - } - - public String getStateBeforeFinishedAsChecked() { - if (states.enabled(BEFORE_BUILD_FINISHED)){ - return CHECKED; - } - return ""; - } - - public String getStateResponsibilityChangedAsChecked() { - if (states.enabled(RESPONSIBILITY_CHANGED)){ - return CHECKED; - } - return ""; - } - - public String getStateBuildInterruptedAsChecked() { - if (states.enabled(BUILD_INTERRUPTED)){ - return CHECKED; - } - return ""; - } - - public String getStateBuildSuccessfulAsChecked() { - if (states.enabled(BUILD_SUCCESSFUL)){ - return CHECKED; - } - return ""; - } - - public String getStateBuildFixedAsChecked() { - if (states.enabled(BUILD_FIXED)){ - return CHECKED; - } - return ""; - } - - public String getStateBuildFailedAsChecked() { - if (states.enabled(BUILD_FAILED)){ - return CHECKED; - } - return ""; - } - - public String getStateBuildBrokenAsChecked() { - if (states.enabled(BUILD_BROKEN)){ - return CHECKED; - } - return ""; - } - - public String getPayloadFormat() { - return payloadFormat; - } - - public String getPayloadTemplate() { - return payloadTemplate; - } - - /** - * Sets the payload format to whatever string is passed. - * It does NOT check that the payload format has a valid implementation loaded. - * - * @param payloadFormat - */ - public void setPayloadFormat(String payloadFormat) { - this.payloadFormat = payloadFormat; - } - - - public void setPayloadTemplate(String payloadTemplate) { - this.payloadTemplate = payloadTemplate; - } - - public Boolean isEnabledForAllBuildsInProject() { - return allBuildTypesEnabled; - } - - public void enableForAllBuildsInProject(Boolean allBuildTypesEnabled) { - this.allBuildTypesEnabled = allBuildTypesEnabled; - } - - public Boolean isEnabledForSubProjects() { - return subProjectsEnabled; - } - - public void enableForSubProjects(Boolean subProjectsEnabled) { - this.subProjectsEnabled = subProjectsEnabled; - } - - public void clearAllEnabledBuildsInProject(){ - this.enabledBuildTypesSet.clear(); - } - - public void enableBuildInProject(String buildTypeId) { - this.enabledBuildTypesSet.add(buildTypeId); - } - - public Map getEnabledTemplates() { - Map mT = WebHookPayloadDefaultTemplates.getDefaultEnabledPayloadTemplates(); - for (CustomMessageTemplate t : templates.values()){ - if (t.enabled){ - mT.put(t.templateType, t.templateText); - } - } - return mT; - } - - public Boolean getAuthEnabled() { - return authEnabled; - } - - public void setAuthEnabled(Boolean authEnabled) { - this.authEnabled = authEnabled; - } - - public void setAuthParameters(Map authParameters) { - this.authParameters.putAll(authParameters); - } - - public void clearAuthParameters() { - this.authParameters.clear(); - } - - public void setAuthType(String authType) { - this.authType = authType; - } - - public void setAuthPreemptive(Boolean authPreemptive) { - this.authPreemptive = authPreemptive; - } - - public WebHookAuthConfig getAuthenticationConfig() { - if (authEnabled && !authType.equals("")){ - WebHookAuthConfig webhookAuthConfig= new WebHookAuthConfig(); - webhookAuthConfig.setType(authType); - webhookAuthConfig.setPreemptive(authPreemptive); - webhookAuthConfig.getParameters().putAll(authParameters); - return webhookAuthConfig; - } - return null; - } - - public List getTriggerFilters() { - return this.filters; - } - - public List getHeaders() { - return headers; - } - - public void setHeaders(List headers) { - this.headers = headers; - } - - public WebHookConfig copy() { - WebHookConfig configCopy = new WebHookConfig(this.getAsElement()) ; - configCopy.setUniqueKey(this.getUniqueKey()); - configCopy.setProjectInternalId(this.getProjectInternalId()); - configCopy.setProjectExternalId(this.getProjectExternalId()); - return configCopy; - } - - public String getProjectInternalId() { - return projectInternalId; - } - - public void setProjectInternalId(String projectInternalId) { - this.projectInternalId = projectInternalId; - } - - public String getProjectExternalId() { - return projectExternalId; - } - - public void setProjectExternalId(String projectExternalId) { - this.projectExternalId = projectExternalId; - } -} +package webhook.teamcity.settings; + +import static webhook.teamcity.BuildStateEnum.BEFORE_BUILD_FINISHED; +import static webhook.teamcity.BuildStateEnum.BUILD_BROKEN; +import static webhook.teamcity.BuildStateEnum.BUILD_FAILED; +import static webhook.teamcity.BuildStateEnum.BUILD_FINISHED; +import static webhook.teamcity.BuildStateEnum.BUILD_FIXED; +import static webhook.teamcity.BuildStateEnum.BUILD_INTERRUPTED; +import static webhook.teamcity.BuildStateEnum.CHANGES_LOADED; +import static webhook.teamcity.BuildStateEnum.BUILD_STARTED; +import static webhook.teamcity.BuildStateEnum.BUILD_SUCCESSFUL; +import static webhook.teamcity.BuildStateEnum.RESPONSIBILITY_CHANGED; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import jetbrains.buildServer.log.Loggers; +import jetbrains.buildServer.serverSide.SBuildType; +import lombok.AllArgsConstructor; +import lombok.Builder; + +import org.apache.commons.lang3.tuple.Pair; +import org.jdom.DataConversionException; +import org.jdom.Element; + +import webhook.teamcity.BuildState; +import webhook.teamcity.BuildStateEnum; +import webhook.teamcity.TeamCityIdResolver; +import webhook.teamcity.auth.WebHookAuthConfig; +import webhook.teamcity.payload.WebHookPayloadDefaultTemplates; +import webhook.teamcity.settings.converter.TemplateToPayloadRollbackConverter; +import webhook.teamcity.settings.converter.WebHookBuildStateConverter; + +@Builder @AllArgsConstructor +public class WebHookConfig { + private static final String EL_TRIGGER_FILTERS = "trigger-filters"; + private static final String EL_HEADERS = "headers"; + private static final String ATTR_PREEMPTIVE = "preemptive"; + private static final String CHECKED = "checked "; + private static final String EL_CUSTOM_TEMPLATE = "custom-template"; + private static final String EL_CUSTOM_TEMPLATES = "custom-templates"; + private static final String ATTR_VALUE = "value"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_PARAM = "param"; + private static final String EL_PARAMETERS = "parameters"; + private static final String ATTR_STATEMASK = "statemask"; + private static final String ATTR_TYPE = "type"; + private static final String EL_STATE = "state"; + private static final String ATTR_TEMPLATE = "template"; + private static final String ATTR_FORMAT = "format"; + private static final String ATTR_ENABLED = "enabled"; + private static final String EL_STATES = "states"; + private static final String EL_BUILD_TYPES = "build-types"; + private static final String ATTR_ENABLED_FOR_ALL = "enabled-for-all"; + private static final String ATTR_ENABLED_FOR_SUBPROJECTS = "enabled-for-subprojects"; + private static final String LOG_PREFIX_WEB_HOOK_CONFIG = "WebHookConfig :: "; + private SortedMap extraParameters; + @Builder.Default private Boolean enabled = true; + @Builder.Default private String uniqueKey = ""; + private String url; + @Builder.Default private String payloadFormat = null; + @Builder.Default private String payloadTemplate = "none"; + @Builder.Default private BuildState states = new BuildState(); + private SortedMap templates; // This defaults to null so that it's not in the XML if empty. + @Builder.Default private Boolean allBuildTypesEnabled = true; + @Builder.Default private Boolean subProjectsEnabled = true; + @Builder.Default private Set enabledBuildTypesSet = new HashSet<>(); + @Builder.Default private String authType = ""; + @Builder.Default private Boolean authEnabled = false; + @Builder.Default private Map authParameters = new LinkedHashMap<>(); + @Builder.Default private Boolean authPreemptive = true; + private List filters; + private List headers; + @Builder.Default private String projectInternalId = null; + @Builder.Default private String projectExternalId = null; + + @SuppressWarnings("unchecked") + public WebHookConfig (Element e) { + + this.uniqueKey = "id_" + getRandomKey(); + this.extraParameters = new TreeMap<>(); + this.states = new BuildState(); + this.templates = new TreeMap<>(); + this.allBuildTypesEnabled = true; + this.subProjectsEnabled = true; + this.enabledBuildTypesSet = new HashSet<>(); + this.authType = ""; + this.authEnabled = false; + this.authParameters = new LinkedHashMap<>(); + this.authPreemptive = true; + this.filters = new ArrayList<>(); + this.headers = new ArrayList<>(); + + if (e.getAttribute("url") != null){ + this.setUrl(e.getAttributeValue("url")); + } + + if (e.getAttribute(ATTR_ENABLED) != null){ + this.setEnabled(Boolean.parseBoolean(e.getAttributeValue(ATTR_ENABLED))); + } + + if (e.getAttribute(ATTR_STATEMASK) != null){ + this.setBuildStates(WebHookBuildStateConverter.convert(Integer.parseInt(e.getAttributeValue(ATTR_STATEMASK)))); + } + + if (e.getAttribute("key") != null){ + this.setUniqueKey(e.getAttributeValue("key")); + } + + if (e.getAttribute(ATTR_FORMAT) != null){ + this.setPayloadFormat(e.getAttributeValue(ATTR_FORMAT)); + } else { + // Set to nvpairs by default for backward compatibility. + this.setPayloadFormat("nvpairs"); + } + + if (e.getAttribute(ATTR_TEMPLATE) != null){ + this.setPayloadTemplate(e.getAttributeValue(ATTR_TEMPLATE)); + } + + if(e.getChild(EL_STATES) != null){ + Element eStates = e.getChild(EL_STATES); + List statesList = eStates.getChildren(EL_STATE); + if ( ! statesList.isEmpty()){ + for(Element eState : statesList) + { + try { + states.setEnabled(BuildStateEnum.findBuildState(eState.getAttributeValue(ATTR_TYPE)), + eState.getAttribute(ATTR_ENABLED).getBooleanValue()); + } catch (DataConversionException e1) { + Loggers.SERVER.warn(LOG_PREFIX_WEB_HOOK_CONFIG + e1.getMessage()); + } + } + } + } + + if(e.getChild(EL_BUILD_TYPES) != null){ + Element eTypes = e.getChild(EL_BUILD_TYPES); + if (eTypes.getAttribute(ATTR_ENABLED_FOR_ALL) != null){ + try { + this.enableForAllBuildsInProject(eTypes.getAttribute(ATTR_ENABLED_FOR_ALL).getBooleanValue()); + } catch (DataConversionException e1) { + Loggers.SERVER.warn(LOG_PREFIX_WEB_HOOK_CONFIG + e1.getMessage()); + } + } + if (eTypes.getAttribute(ATTR_ENABLED_FOR_SUBPROJECTS) != null){ + try { + this.enableForSubProjects(eTypes.getAttribute(ATTR_ENABLED_FOR_SUBPROJECTS).getBooleanValue()); + } catch (DataConversionException e1) { + Loggers.SERVER.warn(LOG_PREFIX_WEB_HOOK_CONFIG + e1.getMessage()); + } + } + if (!isEnabledForAllBuildsInProject()){ + List typesList = eTypes.getChildren("build-type"); + if ( ! typesList.isEmpty()){ + for(Element eType : typesList) + { + if (eType.getAttributeValue("id")!= null){ + enabledBuildTypesSet.add(eType.getAttributeValue("id")); + } + } + } + } + } + + if(e.getChild(EL_PARAMETERS) != null){ + Element eParams = e.getChild(EL_PARAMETERS); + List paramsList = eParams.getChildren(ATTR_PARAM); + if ( ! paramsList.isEmpty()){ + for(Element eParam : paramsList) + { + this.extraParameters.put( + eParam.getAttributeValue(ATTR_NAME), + eParam.getAttributeValue(ATTR_VALUE) + ); + } + } + } + + if(e.getChild(EL_CUSTOM_TEMPLATES) != null){ + Element eParams = e.getChild(EL_CUSTOM_TEMPLATES); + List templateList = eParams.getChildren(EL_CUSTOM_TEMPLATE); + if ( ! templateList.isEmpty()){ + for(Element eParam : templateList) + { + this.templates.put( + eParam.getAttributeValue(CustomMessageTemplate.TYPE), + CustomMessageTemplate.create( + eParam.getAttributeValue(CustomMessageTemplate.TYPE), + eParam.getAttributeValue(CustomMessageTemplate.TEMPLATE), + Boolean.parseBoolean(eParam.getAttributeValue(CustomMessageTemplate.ENABLED)) + ) + ); + } + } + } + + if(e.getChild("auth") != null){ + Element eAuth = e.getChild("auth"); + if (eAuth.getAttribute(ATTR_TYPE) != null){ + // We have an "auth" element + // Try to get the enabled flag + authType = eAuth.getAttribute(ATTR_TYPE).getValue(); + try { + authEnabled = eAuth.getAttribute(ATTR_ENABLED).getBooleanValue(); + } catch (DataConversionException e1){ + // And if it can't be read as boolean default it + // to true anyway (since we have the auth type). + authEnabled = true; + } + try { + if (eAuth.getAttribute(ATTR_PREEMPTIVE) != null){ + authPreemptive = eAuth.getAttribute(ATTR_PREEMPTIVE).getBooleanValue(); + } + } catch (DataConversionException e1){ + // And if it can't be read as boolean default it + // to true (which means creds are always sent). + authPreemptive = true; + } + Element eParams = eAuth.getChild("auth-parameters"); + if (eParams != null){ + List paramsList = eParams.getChildren(ATTR_PARAM); + if (!paramsList.isEmpty()){ + for(Element eParam : paramsList) + { + this.authParameters.put( + eParam.getAttributeValue(ATTR_NAME), + eParam.getAttributeValue(ATTR_VALUE) + ); + } + } + } + } + + } + + /* + + + + */ + if(e.getChild(EL_TRIGGER_FILTERS) != null){ + Element eParams = e.getChild(EL_TRIGGER_FILTERS); + List filterList = eParams.getChildren("filter"); + if (! filterList.isEmpty()){ + for(Element eParam : filterList) + { + this.filters.add( + + WebHookFilterConfig.create( + eParam.getAttributeValue(WebHookFilterConfig.VALUE), + eParam.getAttributeValue(WebHookFilterConfig.REGEX), + Boolean.parseBoolean(eParam.getAttributeValue(WebHookFilterConfig.ENABLED)) + ) + ); + } + } + } + + /* + +
+ + */ + if(e.getChild(EL_HEADERS) != null){ + Element eParams = e.getChild(EL_HEADERS); + List headerList = eParams.getChildren(WebHookHeaderConfig.XML_ELEMENT_NAME); + if (! headerList.isEmpty()){ + for(Element eParam : headerList) + { + this.headers.add( + + WebHookHeaderConfig.create( + eParam.getAttributeValue(WebHookHeaderConfig.NAME), + eParam.getAttributeValue(WebHookHeaderConfig.VALUE) + ) + ); + } + } + } + + this.handle_1_2_Rollback(); + } + + private String getRandomKey() { + int min = 1000000; + int max = 1000000000; + Integer rand = min + new Random().nextInt((max - min) + 1); + return rand.toString(); + } + + /** + * WebHooksConfig constructor. Unchecked version. Use with caution!! + * This constructor does not check if the payloadFormat is valid. + * It will still allow you to add the format, but the webhook might not + * fire at runtime if the payloadFormat configured is not available. + * + * @param url + * @param enabled + * @param stateMask + * @param payloadFormat (unvalidated) + * @param webHookAuthConfig + */ + public WebHookConfig (String projectInternalId, String projectExternalId, String url, Boolean enabled, BuildState states, String payloadFormat, String payloadTemplate, boolean buildTypeAllEnabled, boolean buildTypeSubProjects, Set enabledBuildTypes, WebHookAuthConfig webHookAuthConfig){ + this.uniqueKey = "id_" + getRandomKey(); + this.setProjectInternalId(projectInternalId); + this.setProjectExternalId(projectExternalId); + this.extraParameters = new TreeMap<>(); + this.templates = new TreeMap<>(); + this.authType = ""; + this.authEnabled = false; + this.authParameters = new LinkedHashMap<>(); + this.authPreemptive = true; + this.filters = new ArrayList<>(); + this.headers = new ArrayList<>(); + this.setUrl(url); + this.setEnabled(enabled); + this.setBuildStates(states); + this.setPayloadFormat(payloadFormat); + this.setPayloadTemplate(payloadTemplate); + this.subProjectsEnabled = buildTypeSubProjects; + this.allBuildTypesEnabled = buildTypeAllEnabled; + if (!this.allBuildTypesEnabled){ + this.enabledBuildTypesSet = enabledBuildTypes; + } else { + this.enabledBuildTypesSet = new HashSet<>(); + } + if (webHookAuthConfig != null){ + this.authType = webHookAuthConfig.getType(); + this.authPreemptive = webHookAuthConfig.getPreemptive(); + this.authEnabled = true; + this.authParameters.putAll(webHookAuthConfig.getParameters()); + } + } + + private void handle_1_2_Rollback() { + Pair formatAndTemplate= TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate( + this.getPayloadFormat(), + this.getPayloadTemplate() + ); + this.setPayloadFormat(formatAndTemplate.getLeft()); + this.setPayloadTemplate(formatAndTemplate.getRight()); + } + + private Element getKeyAndValueAsElement(Map map, String key, String elementName){ + Element e = new Element(elementName); + if (map.containsKey(key)){ + e.setAttribute(ATTR_NAME, key); + e.setAttribute(ATTR_VALUE,map.get(key)); + } + return e; + } + + public Element getAsElement(){ + Element el = new Element("webhook"); + el.setAttribute("url", this.getUrl()); + el.setAttribute(ATTR_ENABLED, String.valueOf(this.enabled)); + el.setAttribute(ATTR_FORMAT, String.valueOf(this.payloadFormat).toLowerCase()); + el.setAttribute(ATTR_TEMPLATE, String.valueOf(this.payloadTemplate)); + + Element statesEl = new Element(EL_STATES); + for (BuildStateEnum state : states.getStateSet()){ + Element e = new Element(EL_STATE); + e.setAttribute(ATTR_TYPE, state.getShortName()); + e.setAttribute(ATTR_ENABLED, Boolean.toString(states.enabled(state))); + statesEl.addContent(e); + } + el.addContent(statesEl); + + Element buildsEl = new Element(EL_BUILD_TYPES); + buildsEl.setAttribute(ATTR_ENABLED_FOR_ALL, Boolean.toString(isEnabledForAllBuildsInProject())); + buildsEl.setAttribute(ATTR_ENABLED_FOR_SUBPROJECTS, Boolean.toString(isEnabledForSubProjects())); + + for (String i : enabledBuildTypesSet){ + Element e = new Element("build-type"); + e.setAttribute("id", i); + buildsEl.addContent(e); + } + el.addContent(buildsEl); + + if (this.filters != null && ! this.filters.isEmpty()){ + Element filtersEl = new Element(EL_TRIGGER_FILTERS); + for (WebHookFilterConfig f : this.filters){ + filtersEl.addContent(f.getAsElement()); + } + el.addContent(filtersEl); + } + + if (this.extraParameters.size() > 0){ + Element paramsEl = new Element(EL_PARAMETERS); + for (String i : this.extraParameters.keySet()){ + paramsEl.addContent(this.getKeyAndValueAsElement(this.extraParameters, i, ATTR_PARAM)); + } + el.addContent(paramsEl); + } + + if (this.templates.size() > 0){ + Element templatesEl = new Element(EL_CUSTOM_TEMPLATES); + for (CustomMessageTemplate t : this.templates.values()){ + templatesEl.addContent(t.getAsElement()); + } + el.addContent(templatesEl); + } + + if (this.authType != null && ! this.authType.isEmpty()){ + Element authEl = new Element("auth"); + authEl.setAttribute(ATTR_ENABLED, this.authEnabled.toString()); + authEl.setAttribute(ATTR_TYPE, this.authType); + authEl.setAttribute(ATTR_PREEMPTIVE, this.authPreemptive.toString() ); + if (this.authParameters != null && ! this.authParameters.isEmpty()){ + Element paramsEl = new Element("auth-parameters"); + for (String i : this.authParameters.keySet()){ + paramsEl.addContent(this.getKeyAndValueAsElement(this.authParameters, i, ATTR_PARAM)); + } + authEl.addContent(paramsEl); + } + el.addContent(authEl); + } + + if (this.headers != null && ! this.headers.isEmpty()){ + Element headersEl = new Element(EL_HEADERS); + for (WebHookHeaderConfig h : this.headers){ + headersEl.addContent(h.getAsElement()); + } + el.addContent(headersEl); + } + + return el; + } + + // Getters and Setters.. + + public SortedMap getParams() { + return extraParameters; + } + + public boolean isEnabledForBuildType(SBuildType sBuildType){ + // If allBuildTypes enabled, return true, otherwise return whether the build is in the list of enabled buildTypes. + return isEnabledForAllBuildsInProject() ? true : enabledBuildTypesSet.contains(TeamCityIdResolver.getInternalBuildId(sBuildType)); + } + + public boolean isSpecificBuildTypeEnabled(SBuildType sBuildType){ + // Just check if this build type is only enabled for a specific build. + return enabledBuildTypesSet.contains(TeamCityIdResolver.getInternalBuildId(sBuildType)); + } + + public String getBuildTypeCountAsFriendlyString(){ + if (this.allBuildTypesEnabled && this.subProjectsEnabled){ + return "All builds & Sub-Projects"; + } else if (this.allBuildTypesEnabled){ // this.subProjectsEnabled is false + return "All builds"; + } else { + String subProjectsString = ""; + if (this.subProjectsEnabled){ + subProjectsString = " & All Sub-Project builds"; + } + int enabledBuildTypeCount = this.enabledBuildTypesSet.size(); + if (enabledBuildTypeCount == 1){ + return enabledBuildTypeCount + " build" + subProjectsString; + } + return enabledBuildTypeCount + " builds" + subProjectsString; + } + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public BuildState getBuildStates() { + return states; + } + + public void setBuildStates(BuildState states) { + this.states = states; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUniqueKey() { + return uniqueKey; + } + + public void setUniqueKey(String uniqueKey) { + this.uniqueKey = uniqueKey; + } + + public String getEnabledListAsString(){ + if (!this.enabled){ + return "Disabled"; + } else if (states.allEnabled()){ + return "All Build Events"; + } else if (states.noneEnabled()) { + return "None"; + } else { + String enabledStates = ""; + if (states.enabled(BuildStateEnum.BUILD_STARTED)){ + enabledStates += ", Build Started"; + } + if (states.enabled(BuildStateEnum.CHANGES_LOADED)){ + enabledStates += ", Changes Loaded"; + } + if (states.enabled(BuildStateEnum.BUILD_INTERRUPTED)){ + enabledStates += ", Build Interrupted"; + } + if (states.enabled(BuildStateEnum.BEFORE_BUILD_FINISHED)){ + enabledStates += ", Build Almost Completed"; + } + if (states.enabled(BuildStateEnum.RESPONSIBILITY_CHANGED)){ + enabledStates += ", Build Responsibility Changed"; + } + if (states.enabled(BuildStateEnum.BUILD_FAILED)){ + if (states.enabled(BuildStateEnum.BUILD_BROKEN)){ + enabledStates += ", Build Broken"; + } else { + enabledStates += ", Build Failed"; + } + } + if (states.enabled(BuildStateEnum.BUILD_SUCCESSFUL)){ + if (states.enabled(BuildStateEnum.BUILD_FIXED)){ + enabledStates += ", Build Fixed"; + } else { + enabledStates += ", Build Successful"; + } + } + if (enabledStates.length() > 0){ + return enabledStates.substring(1); + } else { + return "None"; + } + } + } + + public String getWebHookEnabledAsChecked() { + if (this.enabled){ + return CHECKED; + } + return ""; + } + + public String getStateAllAsChecked() { + if (states.allEnabled()){ + return CHECKED; + } + return ""; + } + + public String getStateBuildStartedAsChecked() { + if (states.enabled(BUILD_STARTED)){ + return CHECKED; + } + return ""; + } + + public String getStateChangesLoadedAsChecked() { + if (states.enabled(CHANGES_LOADED)){ + return CHECKED; + } + return ""; + } + + public String getStateBuildFinishedAsChecked() { + if (states.enabled(BUILD_FINISHED)){ + return CHECKED; + } + return ""; + } + + public String getStateBeforeFinishedAsChecked() { + if (states.enabled(BEFORE_BUILD_FINISHED)){ + return CHECKED; + } + return ""; + } + + public String getStateResponsibilityChangedAsChecked() { + if (states.enabled(RESPONSIBILITY_CHANGED)){ + return CHECKED; + } + return ""; + } + + public String getStateBuildInterruptedAsChecked() { + if (states.enabled(BUILD_INTERRUPTED)){ + return CHECKED; + } + return ""; + } + + public String getStateBuildSuccessfulAsChecked() { + if (states.enabled(BUILD_SUCCESSFUL)){ + return CHECKED; + } + return ""; + } + + public String getStateBuildFixedAsChecked() { + if (states.enabled(BUILD_FIXED)){ + return CHECKED; + } + return ""; + } + + public String getStateBuildFailedAsChecked() { + if (states.enabled(BUILD_FAILED)){ + return CHECKED; + } + return ""; + } + + public String getStateBuildBrokenAsChecked() { + if (states.enabled(BUILD_BROKEN)){ + return CHECKED; + } + return ""; + } + + public String getPayloadFormat() { + return payloadFormat; + } + + public String getPayloadTemplate() { + return payloadTemplate; + } + + /** + * Sets the payload format to whatever string is passed. + * It does NOT check that the payload format has a valid implementation loaded. + * + * @param payloadFormat + */ + public void setPayloadFormat(String payloadFormat) { + this.payloadFormat = payloadFormat; + } + + + public void setPayloadTemplate(String payloadTemplate) { + this.payloadTemplate = payloadTemplate; + } + + public Boolean isEnabledForAllBuildsInProject() { + return allBuildTypesEnabled; + } + + public void enableForAllBuildsInProject(Boolean allBuildTypesEnabled) { + this.allBuildTypesEnabled = allBuildTypesEnabled; + } + + public Boolean isEnabledForSubProjects() { + return subProjectsEnabled; + } + + public void enableForSubProjects(Boolean subProjectsEnabled) { + this.subProjectsEnabled = subProjectsEnabled; + } + + public void clearAllEnabledBuildsInProject(){ + this.enabledBuildTypesSet.clear(); + } + + public void enableBuildInProject(String buildTypeId) { + this.enabledBuildTypesSet.add(buildTypeId); + } + + public Map getEnabledTemplates() { + Map mT = WebHookPayloadDefaultTemplates.getDefaultEnabledPayloadTemplates(); + for (CustomMessageTemplate t : templates.values()){ + if (t.enabled){ + mT.put(t.templateType, t.templateText); + } + } + return mT; + } + + public Boolean getAuthEnabled() { + return authEnabled; + } + + public void setAuthEnabled(Boolean authEnabled) { + this.authEnabled = authEnabled; + } + + public void setAuthParameters(Map authParameters) { + this.authParameters.putAll(authParameters); + } + + public void clearAuthParameters() { + this.authParameters.clear(); + } + + public void setAuthType(String authType) { + this.authType = authType; + } + + public void setAuthPreemptive(Boolean authPreemptive) { + this.authPreemptive = authPreemptive; + } + + public WebHookAuthConfig getAuthenticationConfig() { + if (authEnabled && !authType.equals("")){ + WebHookAuthConfig webhookAuthConfig= new WebHookAuthConfig(); + webhookAuthConfig.setType(authType); + webhookAuthConfig.setPreemptive(authPreemptive); + webhookAuthConfig.getParameters().putAll(authParameters); + return webhookAuthConfig; + } + return null; + } + + public List getTriggerFilters() { + return this.filters; + } + + public List getHeaders() { + return headers; + } + + public void setHeaders(List headers) { + this.headers = headers; + } + + public WebHookConfig copy() { + WebHookConfig configCopy = new WebHookConfig(this.getAsElement()) ; + configCopy.setUniqueKey(this.getUniqueKey()); + configCopy.setProjectInternalId(this.getProjectInternalId()); + configCopy.setProjectExternalId(this.getProjectExternalId()); + return configCopy; + } + + public String getProjectInternalId() { + return projectInternalId; + } + + public void setProjectInternalId(String projectInternalId) { + this.projectInternalId = projectInternalId; + } + + public String getProjectExternalId() { + return projectExternalId; + } + + public void setProjectExternalId(String projectExternalId) { + this.projectExternalId = projectExternalId; + } +} diff --git a/tcwebhooks-core/src/main/java/webhook/teamcity/settings/converter/TemplateToPayloadRollbackConverter.java b/tcwebhooks-core/src/main/java/webhook/teamcity/settings/converter/TemplateToPayloadRollbackConverter.java new file mode 100644 index 00000000..c0bc9a5e --- /dev/null +++ b/tcwebhooks-core/src/main/java/webhook/teamcity/settings/converter/TemplateToPayloadRollbackConverter.java @@ -0,0 +1,81 @@ +package webhook.teamcity.settings.converter; + +import java.util.Collection; +import java.util.Map; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.collect.ImmutableMap; + +import webhook.teamcity.payload.format.WebHookPayloadJsonTemplate; + +/** + * A helper class to allow rolling back from 1.2 to 1.1 format in plugin-settings.xml. + * In 1.2, legacy formats are represented by template ids, so rolling back requires + * converting a template id into its old name and format. + * + */ +public class TemplateToPayloadRollbackConverter { + + private TemplateToPayloadRollbackConverter() {} + + public static final String NO_TEMPLATE_ID = "none"; + + public static final String XML_TEMPLATE_ID = "legacy-xml"; + public static final String XML_FORMAT_SHORT_NAME = "xml"; + + public static final String EMPTY_TEMPLATE_ID = "legacy-empty"; + public static final String EMPTY_FORMAT_SHORT_NAME = "empty"; + + public static final String NVPAIRS_TEMPLATE_ID = "legacy-nvpairs"; + public static final String NVPAIRS_FORMAT_SHORT_NAME = "nvpairs"; + + public static final String JSON_TEMPLATE_ID = "legacy-json"; + public static final String JSON_FORMAT_SHORT_NAME = "json"; + + public static final String TAILORED_JSON_TEMPLATE_ID = "legacy-tailored-json"; + public static final String TAILORED_JSON_FORMAT_SHORT_NAME = "tailoredjson"; + + static final Map MAP_OF_TEMPLATES = ImmutableMap.of( + XML_TEMPLATE_ID, XML_FORMAT_SHORT_NAME, + EMPTY_TEMPLATE_ID, EMPTY_FORMAT_SHORT_NAME, + NVPAIRS_TEMPLATE_ID, NVPAIRS_FORMAT_SHORT_NAME, + JSON_TEMPLATE_ID, JSON_FORMAT_SHORT_NAME, + TAILORED_JSON_TEMPLATE_ID, TAILORED_JSON_FORMAT_SHORT_NAME + ); + + static final Collection FORMATS_FROM_1_1 = MAP_OF_TEMPLATES.values(); + + public static Pair transformTemplateToPayloadAndTemplate(String formatId, String templateId) { + + // First check if it's a new format. Need to do this first, since a null format will + // default to NVpairs in 1.1. + if (MAP_OF_TEMPLATES.containsKey(templateId)) { + return Pair.of(MAP_OF_TEMPLATES.get(templateId), NO_TEMPLATE_ID); + } + + // Next check if it's a valid 1.1 template setup correctly. + if (templateId != null + && !templateId.equalsIgnoreCase("none") + && !templateId.isEmpty() + && WebHookPayloadJsonTemplate.FORMAT_SHORT_NAME.equalsIgnoreCase(formatId)) { + + return Pair.of(formatId, templateId); + } + + // Next check if it has a valid template from 1.2 and map it to jsonTemplate. + // By default it will have been nvpairs, which is incorrect + if (templateId != null + && !templateId.equalsIgnoreCase("none") + && !templateId.isEmpty() + && !MAP_OF_TEMPLATES.containsKey(templateId.toLowerCase())) { + + return Pair.of(WebHookPayloadJsonTemplate.FORMAT_SHORT_NAME, templateId); + } + + // If we get here, we have a null template, and a valid 1.1 format + // Just return it so that we don't modify existing webhooks for no reason (eg, set template to none) + return Pair.of(formatId, templateId); + + } +} diff --git a/tcwebhooks-core/src/test/java/webhook/teamcity/payload/template/XMLTemplatesFrom1_2LoadingTest.java b/tcwebhooks-core/src/test/java/webhook/teamcity/payload/template/XMLTemplatesFrom1_2LoadingTest.java new file mode 100644 index 00000000..6279e5ab --- /dev/null +++ b/tcwebhooks-core/src/test/java/webhook/teamcity/payload/template/XMLTemplatesFrom1_2LoadingTest.java @@ -0,0 +1,47 @@ +package webhook.teamcity.payload.template; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.util.List; + +import jetbrains.buildServer.serverSide.SBuildServer; +import jetbrains.buildServer.serverSide.ServerPaths; + +import org.junit.Test; + +import webhook.teamcity.payload.WebHookPayloadManager; +import webhook.teamcity.payload.WebHookPayloadTemplate; +import webhook.teamcity.payload.WebHookTemplateFileChangeHandler; +import webhook.teamcity.payload.WebHookTemplateManager; +import webhook.teamcity.settings.entity.WebHookTemplateJaxHelperImpl; + +public class XMLTemplatesFrom1_2LoadingTest { + + SBuildServer mockServer = mock(SBuildServer.class); + WebHookTemplateManager wtm; + WebHookPayloadManager wpm; + + @Test + public void TestXmlTemplatesCanBeLoadedFrom1_2_ConfigFile(){ + when(mockServer.getRootUrl()).thenReturn("http://test.url"); + wpm = new WebHookPayloadManager(mockServer); + wtm = new WebHookTemplateManager(wpm, new WebHookTemplateJaxHelperImpl()); + + ServerPaths serverPaths = new ServerPaths(new File("src/test/resources/testRollbackDataFrom1.2")); + WebHookTemplateFileChangeHandler changeListener = new WebHookTemplateFileChangeHandler(serverPaths, wtm, wpm, new WebHookTemplateJaxHelperImpl()); + changeListener.register(); + changeListener.handleConfigFileChange(); + + List regsiteredTemplates = wtm.getRegisteredTemplates(); + WebHookPayloadTemplate template = wtm.getTemplate("test-01"); + assertNotNull(template); + assertEquals("jsonTemplate", template.getAsConfig().getFormat()); + assertEquals(4, regsiteredTemplates.size()); + } + +} diff --git a/tcwebhooks-core/src/test/java/webhook/teamcity/settings/converter/TemplateToPayloadRollbackConverterTest.java b/tcwebhooks-core/src/test/java/webhook/teamcity/settings/converter/TemplateToPayloadRollbackConverterTest.java new file mode 100644 index 00000000..46f2a8ea --- /dev/null +++ b/tcwebhooks-core/src/test/java/webhook/teamcity/settings/converter/TemplateToPayloadRollbackConverterTest.java @@ -0,0 +1,133 @@ +package webhook.teamcity.settings.converter; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.lang3.tuple.Pair; +import org.jdom.JDOMException; +import org.junit.Test; + +import webhook.teamcity.settings.WebHookConfig; +import webhook.testframework.util.ConfigLoaderUtil; + +public class TemplateToPayloadRollbackConverterTest { + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_valid_template() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate("jsontemplate", "slack.com-compact"); + assertEquals("jsontemplate", pair.getLeft()); + assertEquals("slack.com-compact",pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_valid_nvpairs_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate("nvpairs", null); + assertEquals("nvpairs", pair.getLeft()); + assertNull(pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_valid_json_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate("json", null); + assertEquals("json", pair.getLeft()); + assertNull(pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_valid_xml_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate("xml", null); + assertEquals("xml", pair.getLeft()); + assertNull(pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_valid_jsontailored_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate("tailoredjson", null); + assertEquals("tailoredjson", pair.getLeft()); + assertNull(pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_valid_json_legacy_format_and_none_template() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate("json", "none"); + assertEquals("json", pair.getLeft()); + assertEquals("none", pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_valid_empty_legacy_format_and_none_template() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate("empty", "none"); + assertEquals("empty", pair.getLeft()); + assertEquals("none", pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_new_xml_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate(null, "legacy-xml"); + assertEquals("xml", pair.getLeft()); + assertEquals("none", pair.getRight()); + } + @Test + public void testTransformTemplateToPayloadAndTemplate_with_new_json_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate(null, "legacy-json"); + assertEquals("json", pair.getLeft()); + assertEquals("none", pair.getRight()); + } + @Test + public void testTransformTemplateToPayloadAndTemplate_with_new_tailoredjson_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate(null, "legacy-tailored-json"); + assertEquals("tailoredjson", pair.getLeft()); + assertEquals("none", pair.getRight()); + } + @Test + public void testTransformTemplateToPayloadAndTemplate_with_new_nvpairs_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate(null, "legacy-nvpairs"); + assertEquals("nvpairs", pair.getLeft()); + assertEquals("none", pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_new_empty_legacy_format() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate(null, "legacy-empty"); + assertEquals("empty", pair.getLeft()); + assertEquals("none", pair.getRight()); + } + + @Test + public void testTransformTemplateToPayloadAndTemplate_with_new_template() { + Pair pair = TemplateToPayloadRollbackConverter.transformTemplateToPayloadAndTemplate(null, "slack.com-compact"); + assertEquals("jsonTemplate", pair.getLeft()); + assertEquals("slack.com-compact",pair.getRight()); + } + + + @Test + public void testLoadingNvPairsConfigWithValid1_1File() throws JDOMException, IOException { + WebHookConfig config = ConfigLoaderUtil.getSpecificWebHookInConfig(1, new File("src/test/resources/project-settings-test-1_2-rollback.xml")); + assertEquals("nvpairs", config.getPayloadFormat()); + assertNull(config.getPayloadTemplate()); + } + + @Test + public void testLoadingJsonConfigWithValid1_1File() throws JDOMException, IOException { + WebHookConfig config = ConfigLoaderUtil.getSpecificWebHookInConfig(2, new File("src/test/resources/project-settings-test-1_2-rollback.xml")); + assertEquals("json", config.getPayloadFormat()); + assertNull(config.getPayloadTemplate()); + } + + @Test + public void testLoadingNvpairsConfigWithValid1_2File() throws JDOMException, IOException { + WebHookConfig config = ConfigLoaderUtil.getSpecificWebHookInConfig(3, new File("src/test/resources/project-settings-test-1_2-rollback.xml")); + assertEquals("nvpairs", config.getPayloadFormat()); + assertEquals("none", config.getPayloadTemplate()); + } + + @Test + public void testLoadingFlowdockConfigWithValid1_2File() throws JDOMException, IOException { + WebHookConfig config = ConfigLoaderUtil.getSpecificWebHookInConfig(4, new File("src/test/resources/project-settings-test-1_2-rollback.xml")); + assertEquals("jsonTemplate", config.getPayloadFormat()); + assertEquals("flowdock", config.getPayloadTemplate()); + } +} diff --git a/tcwebhooks-core/src/test/resources/project-settings-test-1_2-rollback.xml b/tcwebhooks-core/src/test/resources/project-settings-test-1_2-rollback.xml new file mode 100644 index 00000000..8ac524f9 --- /dev/null +++ b/tcwebhooks-core/src/test/resources/project-settings-test-1_2-rollback.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tcwebhooks-core/src/test/resources/testRollbackDataFrom1.2/config/webhook-templates.xml b/tcwebhooks-core/src/test/resources/testRollbackDataFrom1.2/config/webhook-templates.xml new file mode 100644 index 00000000..b3aaf93c --- /dev/null +++ b/tcwebhooks-core/src/test/resources/testRollbackDataFrom1.2/config/webhook-templates.xml @@ -0,0 +1,43 @@ + + + + test01=${buildStatusUrl} + + Test 03 NVPairs standard + + + _Root + + + + test01=${buildStatusUrl} + + Test 04 - NV Pairs Velocity + + + _Root + + + + { + "test01" : "${buildStatusUrl}" +} + + Test 01 - Standard + + + _Root + + + + { + "test01" : "${buildStatusUrl}" +} + + Test 02 - Velocity + + + project1 + + + \ No newline at end of file