Skip to content

Instantly share code, notes, and snippets.

@stefanroeck
Last active January 1, 2024 22:18
Show Gist options
  • Save stefanroeck/0e7b2002eb0e801b8ff619e6738048db to your computer and use it in GitHub Desktop.
Save stefanroeck/0e7b2002eb0e801b8ff619e6738048db to your computer and use it in GitHub Desktop.
XmlFileBasedViolationStore: Store ArchUnit violations in an XML-based VCS-friendly format
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://www.acme.com/archunitviolationstore"
attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:avs="http://www.acme.com/archunitviolationstore"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="store" type="avs:storeType"/>
<xs:complexType name="violationsType">
<xs:sequence>
<xs:element type="xs:string" name="violation" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ruleType">
<xs:sequence>
<xs:element type="avs:violationsType" name="violations"/>
</xs:sequence>
<xs:attribute type="xs:string" name="id" use="required"/>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="category" use="optional"/>
</xs:complexType>
<xs:complexType name="storeType">
<xs:sequence>
<xs:element type="avs:ruleType" name="rule" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.immutables.value.Value;
@Value.Immutable
public interface FreezeRuleConfiguration {
@FunctionalInterface
public interface RuleStore {
RuleStore DEFAULT = () -> "general";
String name();
}
@FunctionalInterface
public interface RuleCategory {
String name();
}
/**
* The rule identifier, must be unique per store.
*/
@Nonnull
String ruleId();
/**
* The rule store or {@link RuleStore#DEFAULT} of omitted.
*/
@Nonnull
@Value.Default
default RuleStore store() {
return RuleStore.DEFAULT;
};
/**
* Optional rule category that can be used to categorize rule violations.
*/
@Nullable
RuleCategory category();
class Builder extends ImmutableFreezeRuleConfiguration.Builder {}
static Builder freezeRuleConfiguration() {
return new Builder();
}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import archunitviolationstore.ObjectFactory;
import archunitviolationstore.StoreType;
class XmlBasedViolationPersistence {
private final File rulesStore;
private final boolean storeCreationAllowed;
private final boolean storeUpdateAllowed;
XmlBasedViolationPersistence(File rulesStore, boolean storeCreationAllowed, boolean storeUpdateAllowed) {
this.rulesStore = rulesStore;
this.storeCreationAllowed = storeCreationAllowed;
this.storeUpdateAllowed = storeUpdateAllowed;
}
StoreType read() {
if (!rulesStore.exists()) {
if (!storeCreationAllowed) {
throw new IllegalStateException(String.format("Violationstore file %s does not exist and storeCreationAllowed=false", rulesStore));
}
return new StoreType();
}
try {
return readFromXmlStore();
} catch (JAXBException | FileNotFoundException | XMLStreamException e) {
throw new IllegalStateException("Error while reading violationstore from file " + rulesStore, e);
}
}
private StoreType readFromXmlStore() throws JAXBException, XMLStreamException, FileNotFoundException {
JAXBContext context = JAXBContext.newInstance(StoreType.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
XMLEventReader reader = xmlInputFactory.createXMLEventReader(new FileInputStream(rulesStore));
return unmarshaller.unmarshal(reader, StoreType.class).getValue();
}
void write(StoreType store) {
if (!storeUpdateAllowed) {
throw new IllegalStateException(String.format("Cannot update violationsStore %s as storeUpdateAllowed=false", rulesStore));
}
try {
writeToXmlStore(store);
} catch (JAXBException | FileNotFoundException e) {
throw new IllegalStateException("Error while writing violationstore to file " + rulesStore, e);
}
}
private void writeToXmlStore(StoreType store) throws JAXBException, FileNotFoundException {
JAXBContext context = JAXBContext.newInstance(StoreType.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(new ObjectFactory().createStore(store), new FileOutputStream(rulesStore));
}
}
import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.emptyList;
import static java.util.Comparator.naturalOrder;
import static java.util.Optional.ofNullable;
import java.io.File;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import archunitviolationstore.RuleType;
import archunitviolationstore.StoreType;
import archunitviolationstore.ViolationsType;
import archunit.lang.ArchRule;
import com.tngtech.archunit.library.freeze.ViolationStore;
class XmlFileBasedViolationStore implements ViolationStore {
private static final Logger LOG = LoggerFactory.getLogger(XmlFileBasedViolationStore.class);
private static final String STORE_PATH_PROPERTY_NAME = "default.path";
private static final String STORE_PATH_DEFAULT = "archunit_store";
private static final String ALLOW_STORE_CREATION_PROPERTY_NAME = "default.allowStoreCreation";
private static final String ALLOW_STORE_CREATION_DEFAULT = "false";
private static final String ALLOW_STORE_UPDATE_PROPERTY_NAME = "default.allowStoreUpdate";
private static final String ALLOW_STORE_UPDATE_DEFAULT = "true";
private final FreezeRuleConfiguration configuration;
private XmlBasedViolationPersistence persistence;
private StoreType storedRules;
XmlFileBasedViolationStore(FreezeRuleConfiguration configuration) {
this.configuration = configuration;
}
@Override
public void initialize(Properties properties) {
// Copied from default store
boolean storeCreationAllowed = Boolean.parseBoolean(properties.getProperty(ALLOW_STORE_CREATION_PROPERTY_NAME, ALLOW_STORE_CREATION_DEFAULT));
boolean storeUpdateAllowed = Boolean.parseBoolean(properties.getProperty(ALLOW_STORE_UPDATE_PROPERTY_NAME, ALLOW_STORE_UPDATE_DEFAULT));
File storeFolder = new File(properties.getProperty(STORE_PATH_PROPERTY_NAME, STORE_PATH_DEFAULT));
ensureExistence(storeFolder);
File storedRulesFile = new File(storeFolder, configuration.store().name() + ".xml");
persistence = new XmlBasedViolationPersistence(storedRulesFile, storeCreationAllowed, storeUpdateAllowed);
storedRules = persistence.read();
LOG.info("Initializing {} at {} containing {} rules.", getClass().getSimpleName(), storedRulesFile.getAbsolutePath(),
storedRules.getRule().size());
}
private void ensureExistence(File folder) {
checkState(folder.exists() && folder.isDirectory() || folder.mkdirs(), "Cannot create folder %s", folder.getAbsolutePath());
}
@Override
public boolean contains(ArchRule rule) {
return getRuleConfig().isPresent();
}
@Override
public void save(ArchRule rule, List<String> violations) {
RuleType ruleType = getRuleConfig().orElseGet(this::createAndAddRuleType);
updateRuleDescription(rule, ruleType);
updateViolations(ruleType, violations);
removeRuleIfEmpty(ruleType);
sortAndPersist();
}
private void updateRuleDescription(ArchRule rule, RuleType ruleType) {
ruleType.setName(cleanse(rule.getDescription()));
}
private String cleanse(String string) {
return string.replaceAll("[<>'\"]", "").replaceAll("[\r\n]", " ").replaceAll(" ", " ");
}
private void sortAndPersist() {
storedRules.getRule().sort(Comparator.comparing(RuleType::getId));
persistence.write(storedRules);
}
private void removeRuleIfEmpty(RuleType ruleType) {
if (ruleType.getViolations().getViolation().isEmpty()) {
storedRules.getRule().remove(ruleType);
}
}
private void updateViolations(RuleType ruleType, List<String> newViolations) {
if (ruleType.getViolations() == null) {
ruleType.setViolations(new ViolationsType());
}
List<String> violationList = ruleType.getViolations().getViolation();
violationList.clear();
violationList.addAll(newViolations);
violationList.sort(naturalOrder());
}
private RuleType createAndAddRuleType() {
RuleType ruleType = new RuleType();
ruleType.setId(configuration.ruleId());
ruleType.setCategory(ofNullable(configuration.category()).map(RuleCategory::name).orElse(null));
storedRules.getRule().add(ruleType);
return ruleType;
}
@Override
public List<String> getViolations(ArchRule rule) {
return getRuleConfig().map(r -> r.getViolations().getViolation()).orElse(emptyList());
}
private Optional<RuleType> getRuleConfig() {
return storedRules.getRule().stream().filter(r -> r.getId().equals(configuration.ruleId())).findFirst();
}
}
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.freeze.FreezingArchRule;
public final class XmlFreezingArchRule {
private XmlFreezingArchRule() {}
public static FreezingArchRule freeze(ArchRule rule, FreezeRuleConfiguration configuration) {
FreezingArchRule frozenRule = FreezingArchRule.freeze(rule);
return frozenRule.persistIn(new XmlFileBasedViolationStore(configuration));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment