Last active
January 1, 2024 22:18
-
-
Save stefanroeck/0e7b2002eb0e801b8ff619e6738048db to your computer and use it in GitHub Desktop.
XmlFileBasedViolationStore: Store ArchUnit violations in an XML-based VCS-friendly format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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