/*
* Copyright 2017 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;
import com.rits.cloning.Cloner;
import com.thoughtworks.go.config.exceptions.GoConfigInvalidException;
import com.thoughtworks.go.config.exceptions.GoConfigInvalidMergeException;
import com.thoughtworks.go.config.parser.ConfigReferenceElements;
import com.thoughtworks.go.config.preprocessor.ConfigParamPreprocessor;
import com.thoughtworks.go.config.preprocessor.ConfigRepoPartialPreprocessor;
import com.thoughtworks.go.config.registry.ConfigElementImplementationRegistry;
import com.thoughtworks.go.config.remote.FileConfigOrigin;
import com.thoughtworks.go.config.validation.*;
import com.thoughtworks.go.domain.ConfigErrors;
import com.thoughtworks.go.security.GoCipher;
import com.thoughtworks.go.util.CachedDigestUtils;
import com.thoughtworks.go.util.ListUtil;
import com.thoughtworks.go.util.SystemEnvironment;
import org.apache.log4j.Logger;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static com.thoughtworks.go.config.parser.GoConfigClassLoader.classParser;
import static com.thoughtworks.go.util.XmlUtils.buildXmlDocument;
import static org.apache.commons.io.IOUtils.toInputStream;
public class MagicalGoConfigXmlLoader {
private static final Logger LOGGER = Logger.getLogger(MagicalGoConfigXmlLoader.class);
public static final List<GoConfigPreprocessor> PREPROCESSORS = Arrays.asList(
new TemplateExpansionPreprocessor(),
new ConfigParamPreprocessor(),
new ConfigRepoPartialPreprocessor());
public static final List<GoConfigValidator> VALIDATORS = Arrays.asList(
new ArtifactDirValidator(),
new EnvironmentAgentValidator(),
new EnvironmentPipelineValidator(),
new ServerIdImmutabilityValidator(),
new CommandRepositoryLocationValidator(new SystemEnvironment())
);
public static final List<GoConfigXMLValidator> XML_VALIDATORS = Arrays.asList((GoConfigXMLValidator)new UniqueOnCancelValidator());
private static final Cloner CLONER = new Cloner();
private ConfigCache configCache;
private final ConfigElementImplementationRegistry registry;
public MagicalGoConfigXmlLoader(ConfigCache configCache, ConfigElementImplementationRegistry registry) {
this.configCache = configCache;
this.registry = registry;
}
public interface Callback {
void call(CruiseConfig cruiseConfig);
}
public GoConfigHolder loadConfigHolder(final String content, Callback callback) throws Exception {
CruiseConfig configForEdit;
CruiseConfig config;
LOGGER.debug("[Config Save] Loading config holder");
configForEdit = deserializeConfig(content);
if (callback != null) callback.call(configForEdit);
config = preprocessAndValidate(configForEdit);
return new GoConfigHolder(config, configForEdit);
}
public GoConfigHolder loadConfigHolder(final String content) throws Exception {
return loadConfigHolder(content, null);
}
public CruiseConfig deserializeConfig(String content) throws Exception {
String md5 = CachedDigestUtils.md5Hex(content);
Element element = parseInputStream(new ByteArrayInputStream(content.getBytes()));
LOGGER.debug("[Config Save] Updating config cache with new XML");
CruiseConfig configForEdit = classParser(element, BasicCruiseConfig.class, configCache, new GoCipher(), registry, new ConfigReferenceElements()).parse();
setMd5(configForEdit, md5);
configForEdit.setOrigins(new FileConfigOrigin());
return configForEdit;
}
public static void setMd5(CruiseConfig configForEdit, String md5) throws NoSuchFieldException, IllegalAccessException {
Field field = BasicCruiseConfig.class.getDeclaredField("md5");
field.setAccessible(true);
field.set(configForEdit, md5);
}
public CruiseConfig preprocessAndValidate(CruiseConfig config) throws Exception {
LOGGER.debug("[Config Save] In preprocessAndValidate: Cloning.");
CruiseConfig cloned = CLONER.deepClone(config);
LOGGER.debug("[Config Save] In preprocessAndValidate: Validating.");
validateCruiseConfig(cloned);
LOGGER.debug("[Config Save] In preprocessAndValidate: Done.");
return cloned;
}
public static List<ConfigErrors> validate(CruiseConfig config) {
preprocess(config);
List<ConfigErrors> validationErrors = new ArrayList<>();
validationErrors.addAll(config.validateAfterPreprocess());
return validationErrors;
}
public static void preprocess(CruiseConfig cruiseConfig) {
for (GoConfigPreprocessor preProcessor : PREPROCESSORS) {
preProcessor.process(cruiseConfig);
}
}
private CruiseConfig validateCruiseConfig(CruiseConfig config) throws Exception {
LOGGER.debug("[Config Save] In validateCruiseConfig: Starting.");
List<ConfigErrors> allErrors = validate(config);
if (!allErrors.isEmpty()) {
if(config.isLocal())
throw new GoConfigInvalidException(config, allErrors.get(0).asString());
else
throw new GoConfigInvalidMergeException("Merged validation failed",config,config.getMergedPartials(),allErrors);
}
LOGGER.debug("[Config Save] In validateCruiseConfig: Running validate.");
for (GoConfigValidator validator : VALIDATORS) {
validator.validate(config);
}
LOGGER.debug("[Config Save] In validateCruiseConfig: Done.");
return config;
}
private Element parseInputStream(InputStream inputStream) throws Exception {
Element rootElement = buildXmlDocument(inputStream, GoConfigSchema.getCurrentSchema(), registry.xsds()).getRootElement();
validateDom(rootElement, registry);
return rootElement;
}
public static void validateDom(Element element, final ConfigElementImplementationRegistry registry) throws Exception {
for (GoConfigXMLValidator xmlValidator : XML_VALIDATORS) {
xmlValidator.validate(element, registry);
}
}
public <T> T fromXmlPartial(String partial, Class<T> o) throws Exception {
return fromXmlPartial(toInputStream(partial), o);
}
public <T> T fromXmlPartial(InputStream inputStream, Class<T> o) throws Exception {
Document document = new SAXBuilder().build(inputStream);
Element element = document.getRootElement();
return classParser(element, o, configCache, new GoCipher(), registry, new ConfigReferenceElements()).parse();
}
public GoConfigPreprocessor getPreprocessorOfType(final Class<? extends com.thoughtworks.go.config.GoConfigPreprocessor> clazz) {
return ListUtil.find(MagicalGoConfigXmlLoader.PREPROCESSORS, new ListUtil.Condition() {
@Override
public <GoConfigPreprocessor> boolean isMet(GoConfigPreprocessor item) {
return item.getClass().isAssignableFrom(clazz);
}
});
}
}