/* * Copyright 2015 ThoughtWorks, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.thoughtworks.go.config.materials; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.thoughtworks.go.config.*; import com.thoughtworks.go.config.preprocessor.SkipParameterResolution; import com.thoughtworks.go.config.validation.NameTypeValidator; import com.thoughtworks.go.domain.ConfigErrors; import com.thoughtworks.go.domain.materials.MaterialConfig; import com.thoughtworks.go.util.CachedDigestUtils; import com.thoughtworks.go.util.ListUtil; import org.apache.commons.lang.StringUtils; /** * @understands material configuration */ public abstract class AbstractMaterialConfig implements MaterialConfig, ParamsAttributeAware { public static final String MATERIAL_NAME = "materialName"; /** * CAREFUL!, this should be the same as the one used in migration 47_create_new_materials.sql */ public static final String FINGERPRINT_DELIMITER = "<|>"; public static final String SQL_CRITERIA_TYPE = "type"; private static final int TRUNCATED_NAME_MAX_LENGTH = 20; public static final String MATERIAL_TYPE = "materialType"; @SkipParameterResolution @ConfigAttribute(value = "materialName", allowNull = true) protected com.thoughtworks.go.config.CaseInsensitiveString name; protected String type; protected ConfigErrors errors = new ConfigErrors(); private Map<String, Object> sqlCriteria; private Map<String, Object> attributesForXml; private String pipelineUniqueFingerprint; private String fingerprint; public AbstractMaterialConfig(String typeName) { type = typeName; } public AbstractMaterialConfig(String typeName, CaseInsensitiveString name, ConfigErrors errors) { this(typeName); this.name = name; this.errors = errors; } @Override public CaseInsensitiveString getName() { return name; } public CaseInsensitiveString getMaterialName() { return name; } @Override public final Map<String, Object> getSqlCriteria() { if (sqlCriteria == null) { Map<String, Object> map = new LinkedHashMap<>(); map.put("type", type); appendCriteria(map); sqlCriteria = Collections.unmodifiableMap(map); } return sqlCriteria; } @Override public String getFingerprint() { if (fingerprint == null) { fingerprint = generateFingerprintFromCriteria(getSqlCriteria()); } return fingerprint; } @Override public String getPipelineUniqueFingerprint() { if (pipelineUniqueFingerprint == null) { Map<String, Object> basicCriteria = new LinkedHashMap<>(getSqlCriteria()); appendPipelineUniqueCriteria(basicCriteria); pipelineUniqueFingerprint = generateFingerprintFromCriteria(basicCriteria); } return pipelineUniqueFingerprint; } private String generateFingerprintFromCriteria(Map<String, Object> sqlCriteria) { List<String> list = new ArrayList<>(); for (Map.Entry<String, Object> criteria : sqlCriteria.entrySet()) { list.add(new StringBuilder().append(criteria.getKey()).append("=").append(criteria.getValue()).toString()); } String fingerprint = ListUtil.join(list, FINGERPRINT_DELIMITER); // CAREFUL! the hash algorithm has to be same as the one used in 47_create_new_materials.sql return CachedDigestUtils.sha256Hex(fingerprint); } @Override public String getTruncatedDisplayName() { String displayName = getDisplayName(); if (displayName.length() > TRUNCATED_NAME_MAX_LENGTH) { StringBuffer buffer = new StringBuffer(); buffer.append(displayName.substring(0, TRUNCATED_NAME_MAX_LENGTH / 2)); buffer.append("..."); buffer.append(displayName.substring(displayName.length() - TRUNCATED_NAME_MAX_LENGTH / 2)); displayName = buffer.toString(); } return displayName; } protected abstract void appendCriteria(Map<String, Object> parameters); protected abstract void appendAttributes(Map<String,Object> parameters); protected abstract void appendPipelineUniqueCriteria(Map<String, Object> basicCriteria); @Override public void setName(final CaseInsensitiveString name) { this.name = name; } @Override public final String getType() { return type; } @Override public String getShortRevision(String revision) { return revision; } @Override public final void validate(ValidationContext validationContext) { if (name != null && !StringUtils.isBlank(CaseInsensitiveString.str(name)) && !new NameTypeValidator().isNameValid(name)) { errors().add(MATERIAL_NAME, NameTypeValidator.errorMessage("material", name)); } validateConcreteMaterial(validationContext); } @Override public void validateTree(PipelineConfigSaveValidationContext validationContext) { validate(validationContext); validateExtras(validationContext); } protected void validateExtras(ValidationContext validationContext){ } protected abstract void validateConcreteMaterial(ValidationContext validationContext); @Override public ConfigErrors errors() { return errors; } @Override public void addError(String fieldName, String message) { errors.add(fieldName, message); } @Override public void validateNameUniqueness(Map<CaseInsensitiveString, AbstractMaterialConfig> map) { if (CaseInsensitiveString.isBlank(getName())) { return; } CaseInsensitiveString currentMaterialName = getName(); AbstractMaterialConfig materialWithSameName = map.get(currentMaterialName); if (materialWithSameName != null) { materialWithSameName.addNameConflictError(); addNameConflictError(); return; } map.put(currentMaterialName, this); } @Override public boolean isSameFlyweight(MaterialConfig other) { return getFingerprint().equals(other.getFingerprint()); } private void addNameConflictError() { errors.add("materialName", String.format( "You have defined multiple materials called '%s'. " + "Material names are case-insensitive and must be unique. " + "Note that for dependency materials the default materialName is the name of the upstream pipeline. You can override this by setting the materialName explicitly for the upstream pipeline.", getDisplayName())); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } AbstractMaterialConfig that = (AbstractMaterialConfig) o; if (name != null ? !name.equals(that.name) : that.name != null) { return false; } if (type != null ? !type.equals(that.type) : that.type != null) { return false; } return true; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (type != null ? type.hashCode() : 0); return result; } @Override public String toString() { return String.format("AbstractMaterial{name=%s, type=%s}", name, type); } @Override public void setConfigAttributes(Object attributes) { resetCachedIdentityAttributes();//TODO: BUG: update this after making changes to attributes, because this is thread unsafe if primed by another thread when initialization is half way through(and the returning API will use inconsistent and temprory value) --sara & jj Map map = (Map) attributes; if (map.containsKey(MATERIAL_NAME)) { String name = (String) map.get(MATERIAL_NAME); this.name = StringUtils.isBlank(name) ? null : new CaseInsensitiveString(name); } } @Override public boolean isUsedInLabelTemplate(PipelineConfig pipelineConfig) { CaseInsensitiveString materialName = getName(); return materialName != null && pipelineConfig.getLabelTemplate().toLowerCase().contains(String.format("${%s}", materialName.toLower())); } protected void resetCachedIdentityAttributes() { sqlCriteria = null; attributesForXml = null; pipelineUniqueFingerprint = null; } }