chore: Various bean validation improvements

This commit is contained in:
Robin Shen 2025-09-22 11:00:11 +08:00
parent a6fb15d7a9
commit 77cf1da58c
44 changed files with 1051 additions and 383 deletions

View File

@ -641,8 +641,8 @@
</repository>
</repositories>
<properties>
<commons.version>3.0.13</commons.version>
<agent.version>2.2.18</agent.version>
<commons.version>3.0.14</commons.version>
<agent.version>2.2.19</agent.version>
<slf4j.version>2.0.9</slf4j.version>
<logback.version>1.4.14</logback.version>
<antlr.version>4.7.2</antlr.version>

View File

@ -3,6 +3,7 @@ package io.onedev.server;
import static com.google.common.collect.Lists.newArrayList;
import java.io.Serializable;
import java.lang.annotation.ElementType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@ -23,6 +24,9 @@ import javax.persistence.OneToOne;
import javax.persistence.Transient;
import javax.persistence.Version;
import javax.validation.Configuration;
import javax.validation.Path;
import javax.validation.Path.Node;
import javax.validation.TraversableResolver;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
@ -71,6 +75,7 @@ import io.onedev.commons.utils.ExceptionUtils;
import io.onedev.commons.utils.StringUtils;
import io.onedev.k8shelper.KubernetesHelper;
import io.onedev.k8shelper.OsInfo;
import io.onedev.server.annotation.Shallow;
import io.onedev.server.attachment.AttachmentManager;
import io.onedev.server.attachment.DefaultAttachmentManager;
import io.onedev.server.buildspec.BuildSpecSchemaResource;
@ -382,6 +387,7 @@ import io.onedev.server.util.xstream.ReflectionConverter;
import io.onedev.server.util.xstream.StringConverter;
import io.onedev.server.util.xstream.VersionedDocumentConverter;
import io.onedev.server.validation.MessageInterpolator;
import io.onedev.server.validation.ShallowValidatorProvider;
import io.onedev.server.validation.ValidatorProvider;
import io.onedev.server.web.DefaultUrlManager;
import io.onedev.server.web.DefaultWicketFilter;
@ -446,8 +452,31 @@ public class CoreModule extends AbstractPluginModule {
.messageInterpolator(new MessageInterpolator());
return configuration.buildValidatorFactory();
}).in(Singleton.class);
bind(ValidatorFactory.class).annotatedWith(Shallow.class).toProvider(() -> {
Configuration<?> configuration = Validation
.byDefaultProvider()
.configure()
.traversableResolver(new TraversableResolver() {
@Override
public boolean isReachable(Object traversableObject, Node traversableProperty,
Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
return true;
}
@Override
public boolean isCascadable(Object traversableObject, Node traversableProperty,
Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
return false;
}
})
.messageInterpolator(new MessageInterpolator());
return configuration.buildValidatorFactory();
}).in(Singleton.class);
bind(Validator.class).toProvider(ValidatorProvider.class).in(Singleton.class);
bind(Validator.class).annotatedWith(Shallow.class).toProvider(ShallowValidatorProvider.class).in(Singleton.class);
configurePersistence();
configureSecurity();

View File

@ -10,6 +10,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.ServerSocket;
@ -365,6 +366,10 @@ public class OneDev extends AbstractPlugin implements Serializable, Runnable {
return AppLoader.getInstance(type);
}
public static <T> T getInstance(Class<T> type, Class<? extends Annotation> annotationClass) {
return AppLoader.getInstance(type, annotationClass);
}
public static <T> Set<T> getExtensions(Class<T> extensionPoint) {
return AppLoader.getExtensions(extensionPoint);
}

View File

@ -14,4 +14,5 @@ public @interface DependsOn {
String value() default "";
boolean inverse() default false;
}

View File

@ -0,0 +1,12 @@
package io.onedev.server.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import com.google.inject.BindingAnnotation;
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface Shallow {
}

View File

@ -1,10 +1,42 @@
package io.onedev.server.buildspec;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.ValidationException;
import javax.validation.Validator;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.wicket.Component;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.nodes.Tag;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import io.onedev.commons.codeassist.InputCompletion;
import io.onedev.commons.codeassist.InputStatus;
import io.onedev.commons.codeassist.InputSuggestion;
@ -33,21 +65,6 @@ import io.onedev.server.validation.Validatable;
import io.onedev.server.web.page.project.blob.ProjectBlobPage;
import io.onedev.server.web.util.SuggestionUtils;
import io.onedev.server.web.util.WicketUtils;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.wicket.Component;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.nodes.*;
import javax.annotation.Nullable;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintViolation;
import javax.validation.ValidationException;
import javax.validation.Validator;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
@Editable
@ClassValidating
@ -105,6 +122,7 @@ public class BuildSpec implements Serializable, Validatable {
private transient Map<String, JobProperty> propertyMap;
@Editable
@Valid
public List<Job> getJobs() {
return jobs;
}
@ -115,6 +133,7 @@ public class BuildSpec implements Serializable, Validatable {
}
@Editable
@Valid
public List<StepTemplate> getStepTemplates() {
return stepTemplates;
}
@ -125,6 +144,7 @@ public class BuildSpec implements Serializable, Validatable {
}
@Editable
@Valid
public List<Service> getServices() {
return services;
}
@ -135,6 +155,7 @@ public class BuildSpec implements Serializable, Validatable {
}
@Editable
@Valid
public List<JobProperty> getProperties() {
return properties;
}
@ -145,6 +166,7 @@ public class BuildSpec implements Serializable, Validatable {
}
@Editable
@Valid
public List<Import> getImports() {
return imports;
}
@ -271,27 +293,10 @@ public class BuildSpec implements Serializable, Validatable {
return isValid;
}
private Validator getValidator() {
return OneDev.getInstance(Validator.class);
}
@Override
public boolean isValid(ConstraintValidatorContext context) {
boolean isValid = true;
int index = 0;
for (Job job: jobs) {
for (ConstraintViolation<Job> violation: getValidator().validate(job)) {
context.buildConstraintViolationWithTemplate(violation.getMessage())
.addPropertyNode(PROP_JOBS)
.addPropertyNode(violation.getPropertyPath().toString())
.inIterable().atIndex(index)
.addConstraintViolation();
isValid = false;
}
index++;
}
Set<String> jobNames = new HashSet<>();
for (Job job: jobs) {
if (!jobNames.add(job.getName())) {
@ -300,19 +305,6 @@ public class BuildSpec implements Serializable, Validatable {
isValid = false;
}
}
index = 0;
for (Service service: services) {
for (ConstraintViolation<Service> violation: getValidator().validate(service)) {
context.buildConstraintViolationWithTemplate(violation.getMessage())
.addPropertyNode(PROP_SERVICES)
.addPropertyNode(violation.getPropertyPath().toString())
.inIterable().atIndex(index)
.addConstraintViolation();
isValid = false;
}
index++;
}
Set<String> serviceNames = new HashSet<>();
for (Service service: services) {
@ -322,19 +314,6 @@ public class BuildSpec implements Serializable, Validatable {
isValid = false;
}
}
index = 0;
for (StepTemplate stepTemplate: stepTemplates) {
for (ConstraintViolation<StepTemplate> violation: getValidator().validate(stepTemplate)) {
context.buildConstraintViolationWithTemplate(violation.getMessage())
.addPropertyNode(PROP_STEP_TEMPLATES)
.addPropertyNode(violation.getPropertyPath().toString())
.inIterable().atIndex(index)
.addConstraintViolation();
isValid = false;
}
index++;
}
Set<String> stepTemplateNames = new HashSet<>();
for (StepTemplate template: stepTemplates) {
@ -344,19 +323,6 @@ public class BuildSpec implements Serializable, Validatable {
isValid = false;
}
}
index = 0;
for (JobProperty property: properties) {
for (ConstraintViolation<JobProperty> violation: getValidator().validate(property)) {
context.buildConstraintViolationWithTemplate(violation.getMessage())
.addPropertyNode(PROP_PROPERTIES)
.addPropertyNode(violation.getPropertyPath().toString())
.inIterable().atIndex(index)
.addConstraintViolation();
isValid = false;
}
index++;
}
Set<String> propertyNames = new HashSet<>();
for (JobProperty property : properties) {
@ -366,20 +332,6 @@ public class BuildSpec implements Serializable, Validatable {
isValid = false;
}
}
index = 0;
for (Import aImport: getImports()) {
Validator validator = OneDev.getInstance(Validator.class);
for (ConstraintViolation<Import> violation: validator.validate(aImport)) {
context.buildConstraintViolationWithTemplate(violation.getMessage())
.addPropertyNode(PROP_IMPORTS)
.addPropertyNode(violation.getPropertyPath().toString())
.inIterable().atIndex(index)
.addConstraintViolation();
isValid = false;
}
index++;
}
Set<String> importProjectAndRevisions = new HashSet<>();
for (Import aImport: imports) {
@ -2335,4 +2287,113 @@ public class BuildSpec implements Serializable, Validatable {
}
}
@SuppressWarnings("unused")
private void migrate42(VersionedYamlDoc doc, Stack<Integer> versions) {
migrate42_processNode(doc);
}
private void migrate42_processNode(Node node) {
if (node instanceof MappingNode) {
MappingNode mappingNode = (MappingNode) node;
if (mappingNode.getTag() != null && !mappingNode.getTag().equals(Tag.MAP)
&& mappingNode.getTag().getValue().startsWith("!")) {
var tagValue = mappingNode.getTag().getValue().substring(1);
boolean hasTypeTuple = false;
for (NodeTuple tuple : mappingNode.getValue()) {
if (tuple.getKeyNode() instanceof ScalarNode) {
ScalarNode keyNode = (ScalarNode) tuple.getKeyNode();
if ("type".equals(keyNode.getValue())) {
hasTypeTuple = true;
break;
}
}
}
if (!hasTypeTuple) {
mappingNode.getValue().add(0, new NodeTuple(
new ScalarNode(Tag.STR, "type"),
new ScalarNode(Tag.STR, tagValue)));
}
mappingNode.setTag(Tag.MAP);
}
for (NodeTuple tuple : mappingNode.getValue()) {
migrate42_processNode(tuple.getKeyNode());
migrate42_processNode(tuple.getValueNode());
}
} else if (node instanceof SequenceNode) {
SequenceNode sequenceNode = (SequenceNode) node;
for (Node item : sequenceNode.getValue()) {
migrate42_processNode(item);
}
}
}
@SuppressWarnings("unused")
private void migrate43(VersionedYamlDoc doc, Stack<Integer> versions) {
for (NodeTuple specTuple: doc.getValue()) {
String specKey = ((ScalarNode)specTuple.getKeyNode()).getValue();
if (specKey.equals("jobs")) {
SequenceNode jobsNode = (SequenceNode) specTuple.getValueNode();
for (Node jobsNodeItem: jobsNode.getValue()) {
MappingNode jobNode = (MappingNode) jobsNodeItem;
boolean hasRetryCondition = false;
for (var itJobTuple = jobNode.getValue().iterator(); itJobTuple.hasNext();) {
var jobTuple = itJobTuple.next();
var keyNode = (ScalarNode) jobTuple.getKeyNode();
if (keyNode.getValue().equals("retryCondition")) {
var valueNode = (ScalarNode) jobTuple.getValueNode();
if (StringUtils.isBlank(valueNode.getValue())) {
itJobTuple.remove();
} else {
hasRetryCondition = true;
}
break;
} else if (keyNode.getValue().equals("triggers")) {
SequenceNode triggersNode = (SequenceNode) jobTuple.getValueNode();
for (Node triggerNode: triggersNode.getValue()) {
MappingNode triggerMappingNode = (MappingNode) triggerNode;
boolean hasUserMatch = false;
String triggerType = null;
for (var triggerTuple: triggerMappingNode.getValue()) {
String triggerTupleKey = ((ScalarNode)triggerTuple.getKeyNode()).getValue();
if (triggerTupleKey.equals("type")) {
triggerType = ((ScalarNode)triggerTuple.getValueNode()).getValue();
break;
}
}
if ("BranchUpdateTrigger".equals(triggerType)) {
for (var itTriggerTuple = triggerMappingNode.getValue().iterator(); itTriggerTuple.hasNext();) {
var triggerTuple = itTriggerTuple.next();
String triggerTupleKey = ((ScalarNode)triggerTuple.getKeyNode()).getValue();
if (triggerTupleKey.equals("userMatch")) {
var valueNode = (ScalarNode) triggerTuple.getValueNode();
if (StringUtils.isBlank(valueNode.getValue())) {
itTriggerTuple.remove();
} else {
hasUserMatch = true;
}
break;
}
}
if (!hasUserMatch) {
triggerMappingNode.getValue().add(new NodeTuple(
new ScalarNode(Tag.STR, "userMatch"),
new ScalarNode(Tag.STR, "anyone")));
}
}
}
}
}
if (!hasRetryCondition) {
jobNode.getValue().add(new NodeTuple(
new ScalarNode(Tag.STR, "retryCondition"),
new ScalarNode(Tag.STR, "never")));
}
}
}
}
}
}

View File

@ -1,13 +1,20 @@
package io.onedev.server.buildspec;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -17,21 +24,31 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SerializationUtils;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import io.onedev.commons.loader.ImplementationRegistry;
import io.onedev.commons.utils.ClassUtils;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.annotation.Code;
import io.onedev.server.annotation.DependsOn;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.ImplementationProvider;
import io.onedev.server.annotation.Interpolative;
import io.onedev.server.annotation.Multiline;
import io.onedev.server.annotation.Patterns;
import io.onedev.server.annotation.RetryCondition;
import io.onedev.server.annotation.UserMatch;
import io.onedev.server.buildspec.job.Job;
import io.onedev.server.data.migration.MigrationHelper;
import io.onedev.server.model.support.build.JobProperty;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.util.Pair;
import io.onedev.server.util.ReflectionUtils;
import io.onedev.server.util.patternset.PatternSet;
import io.onedev.server.web.editable.BeanDescriptor;
import io.onedev.server.web.editable.EditableUtils;
import io.onedev.server.web.editable.PropertyDescriptor;
@ -60,40 +77,167 @@ public class BuildSpecSchemaResource {
this.yaml = new Yaml(options);
}
private void processProperty(Map<String, Object> currentNode, PropertyDescriptor property) {
var description = property.getDescription();
if (description != null)
currentNode.put("description", description);
private void processProperty(Map<String, Object> currentNode, Object bean, PropertyDescriptor property) {
var descriptionSections = new ArrayList<String>();
var descriptionSection = property.getDescription();
if (descriptionSection != null)
descriptionSections.add(descriptionSection);
var getter = property.getPropertyGetter();
var returnType = property.getPropertyClass();
if (returnType == String.class) {
if (getter.getAnnotation(Code.class) == null && getter.getAnnotation(Multiline.class) == null) {
descriptionSections.add("NOTE: If set, the value can only contain one line");
}
InputStream grammarStream = null;
if (getter.getAnnotation(Patterns.class) != null) {
if (getter.getAnnotation(Interpolative.class) != null) {
grammarStream = PatternSet.class.getResourceAsStream("InterpolativePatternSet.g4");
} else {
grammarStream = PatternSet.class.getResourceAsStream("PatternSet.g4");
}
} else if (getter.getAnnotation(RetryCondition.class) != null) {
grammarStream = io.onedev.server.buildspec.job.retrycondition.RetryCondition.class.getResourceAsStream("RetryCondition.g4");
} else if (getter.getAnnotation(UserMatch.class) != null) {
grammarStream = io.onedev.server.util.usermatch.UserMatch.class.getResourceAsStream("UserMatch.g4");
}
if (grammarStream != null) {
try {
var grammar = IOUtils.toString(grammarStream, StandardCharsets.UTF_8);
descriptionSections.add("NOTE: If set, the value should conform with below ANTLR v4 grammar:\n\n" + grammar);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
if (descriptionSections.size() != 0)
currentNode.put("description", StringUtils.join(descriptionSections, "\n\n"));
Class<?> elementClass = null;
if (Collection.class.isAssignableFrom(returnType)) {
var elementClass = ReflectionUtils.getCollectionElementClass(property.getPropertyGetter().getGenericReturnType());
elementClass = ReflectionUtils.getCollectionElementClass(property.getPropertyGetter().getGenericReturnType());
if (elementClass == null)
throw new ExplicitException("Unknown collection element class (bean: " + property.getBeanClass() + ", property: " + property.getPropertyName() + ")");
processCollectionProperty(currentNode, elementClass);
} else {
processType(currentNode, returnType);
}
Object defaultValue;
var value = property.getPropertyValue(bean);
if (value instanceof Integer) {
var intValue = (Integer) value;
if (intValue == 0)
defaultValue = null;
else
defaultValue = intValue;
} else if (value instanceof Long) {
var longValue = (Long) value;
if (longValue == 0)
defaultValue = null;
else
defaultValue = longValue;
} else if (value instanceof Double) {
var doubleValue = (Double) value;
if (doubleValue == 0)
defaultValue = null;
else
defaultValue = doubleValue;
} else if (value instanceof Float) {
var floatValue = (Float) value;
if (floatValue == 0)
defaultValue = null;
else
defaultValue = floatValue;
} else if (value instanceof Boolean) {
var booleanValue = (Boolean) value;
if (!booleanValue)
defaultValue = null;
else
defaultValue = booleanValue;
} else if (value instanceof Enum) {
var enumValue = (Enum<?>) value;
defaultValue = enumValue.name();
} else if (value instanceof String || value instanceof Date) {
defaultValue = value;
} else {
defaultValue = null;
}
if (defaultValue != null) {
currentNode.put("default", defaultValue);
}
}
private Object newBean(Class<?> beanClass) {
try {
return beanClass.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private void processBean(Map<String, Object> currentNode, Class<?> beanClass,
Map<String, PropertyDescriptor> processedProperties) {
Collection<Class<?>> implementations, Set<String> processedProperties) {
var propsNode = (Map<String, Object>) currentNode.get("properties");
if (propsNode == null) {
propsNode = new HashMap<>();
currentNode.put("properties", propsNode);
}
var requiredNode = (List<String>) currentNode.get("required");
if (requiredNode == null) {
requiredNode = new ArrayList<>();
currentNode.put("required", requiredNode);
}
var beanDescriptor = new BeanDescriptor(beanClass);
var propertyMap = new HashMap<String, PropertyDescriptor>();
for (var groupProperties: beanDescriptor.getProperties().values()) {
for (var property: groupProperties) {
propertyMap.put(property.getPropertyName(), property);
}
}
/*
* As long as one implementation overrides a property in base class, we will exclude the property from
* common property section, and add it individually to each implementation's property section to avoid
* defining same property both in common section and oneOf section
*/
var excludedProperties = new HashSet<String>();
Map<String, byte[]> valueBytesMap = new HashMap<>();
Object bean = null;
for (var implementation: implementations) {
bean = newBean(implementation);
for (var groupProperties: new BeanDescriptor(implementation).getProperties().values()) {
for (var property: groupProperties) {
if (!excludedProperties.contains(property.getPropertyName())
&& propertyMap.containsKey(property.getPropertyName())
&& property.getBeanClass() != beanClass) {
excludedProperties.add(property.getPropertyName());
}
if (!excludedProperties.contains(property.getPropertyName())) {
var value = property.getPropertyValue(bean);
var lastValueBytes = valueBytesMap.get(property.getPropertyName());
if (lastValueBytes == null) {
lastValueBytes = SerializationUtils.serialize((Serializable) value);
valueBytesMap.put(property.getPropertyName(), lastValueBytes);
} else if (!Arrays.equals(lastValueBytes, SerializationUtils.serialize((Serializable) value))) {
excludedProperties.add(property.getPropertyName());
}
}
}
}
}
if (bean == null) {
bean = newBean(beanClass);
}
var dependents = new ArrayList<Pair<PropertyDescriptor, DependsOn>>();
for (var groupProperties: new BeanDescriptor(beanClass).getProperties().values()) {
for (var groupProperties: beanDescriptor.getProperties().values()) {
for (var property: groupProperties) {
if (processedProperties.putIfAbsent(property.getPropertyName(), property) == null) {
if (!excludedProperties.contains(property.getPropertyName()) && processedProperties.add(property.getPropertyName())) {
if (property.getPropertyName().equals("type"))
throw new ExplicitException("Property 'type' is reserved (class: " + beanClass.getName() + ")");
var dependsOn = property.getPropertyGetter().getAnnotation(DependsOn.class);
@ -104,7 +248,7 @@ public class BuildSpecSchemaResource {
requiredNode.add(property.getPropertyName());
var propNode = new HashMap<String, Object>();
propsNode.put(property.getPropertyName(), propNode);
processProperty(propNode, property);
processProperty(propNode, bean, property);
}
}
}
@ -116,7 +260,7 @@ public class BuildSpecSchemaResource {
var allOfItemNode = new HashMap<String, Object>();
allOfNode.add(allOfItemNode);
var dependsOn = dependent.getRight();
var dependencyProperty = processedProperties.get(dependsOn.property());
var dependencyProperty = propertyMap.get(dependsOn.property());
if (dependencyProperty == null)
throw new ExplicitException("Dependency property not found: " + dependsOn.property());
@ -176,7 +320,7 @@ public class BuildSpecSchemaResource {
branchNode.put("properties", branchPropsNode);
var propNode = new HashMap<String, Object>();
branchPropsNode.put(property.getPropertyName(), propNode);
processProperty(propNode, property);
processProperty(propNode, bean, property);
if (property.isPropertyRequired()) {
var requiredList = new ArrayList<String>();
requiredList.add(property.getPropertyName());
@ -184,6 +328,11 @@ public class BuildSpecSchemaResource {
}
}
}
if (!propsNode.isEmpty())
currentNode.put("properties", propsNode);
if (!requiredNode.isEmpty())
currentNode.put("required", requiredNode);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@ -210,7 +359,7 @@ public class BuildSpecSchemaResource {
} else if (type.getAnnotation(Editable.class) != null) {
if (ClassUtils.isConcrete(type)) {
currentNode.put("type", "object");
processBean(currentNode, type, new HashMap<>());
processBean(currentNode, type, new ArrayList<>(), new HashSet<>());
currentNode.put("additionalProperties", false);
} else {
processPolymorphic(currentNode, type);
@ -232,10 +381,10 @@ public class BuildSpecSchemaResource {
currentNode.put("type", "object");
var propsNode = new HashMap<String, Object>();
currentNode.put("properties", propsNode);
var typeNode = new HashMap<String, Object>();
propsNode.put("type", typeNode);
typeNode.put("type", "string");
propsNode.put("type", typeNode);
currentNode.put("properties", propsNode);
var enumList = new ArrayList<String>();
typeNode.put("enum", enumList);
@ -244,8 +393,8 @@ public class BuildSpecSchemaResource {
requiredList.add("type");
currentNode.put("required", requiredList);
var processedProperties = new HashMap<String, PropertyDescriptor>();
processBean(currentNode, baseClass, processedProperties);
var processedProperties = new HashSet<String>();
processBean(currentNode, baseClass, implementations, processedProperties);
var oneOfList = new ArrayList<Map<String, Object>>();
currentNode.put("oneOf", oneOfList);
@ -253,15 +402,16 @@ public class BuildSpecSchemaResource {
for (var implementation: implementations) {
enumList.add(implementation.getSimpleName());
var oneOfItemNode = new HashMap<String, Object>();
oneOfList.add(oneOfItemNode);
var oneOfItemPropsNode = new HashMap<String, Object>();
var typeConstNode = new HashMap<String, Object>();
typeConstNode.put("const", implementation.getSimpleName());
oneOfItemPropsNode.put("type", typeConstNode);
oneOfItemNode.put("properties", oneOfItemPropsNode);
var description = EditableUtils.getDescription(implementation);
if (description != null)
oneOfItemNode.put("description", description);
processBean(oneOfItemNode, implementation, new HashMap<>(processedProperties));
var implementationPropsNode = (Map<String, Object>) oneOfItemNode.get("properties");
var typeConstNode = new HashMap<String, Object>();
typeConstNode.put("const", implementation.getSimpleName());
implementationPropsNode.put("type", typeConstNode);
processBean(oneOfItemNode, implementation, new ArrayList<>(), new HashSet<>(processedProperties));
oneOfList.add(oneOfItemNode);
}
}
@ -279,7 +429,6 @@ public class BuildSpecSchemaResource {
}
}
@Path("/")
@GET
@SuppressWarnings("unchecked")
public String getBuildSpecSchema() {

View File

@ -64,7 +64,7 @@ public class Import implements Serializable, Validatable {
private transient BuildSpec buildSpec;
private static ThreadLocal<Stack<String>> importChain = ThreadLocal.withInitial(Stack::new);
private static ThreadLocal<Stack<String>> IMPORT_CHAIN = ThreadLocal.withInitial(Stack::new);
// change Named("projectPath") also if change name of this property
@Editable(order=100, name="Project", description="Specify project to import build spec from")
@ -194,8 +194,8 @@ public class Import implements Serializable, Validatable {
public boolean isValid(ConstraintValidatorContext context) {
try {
var commit = getCommit();
if (importChain.get().contains(commit.name())) {
List<String> circular = new ArrayList<>(importChain.get());
if (IMPORT_CHAIN.get().contains(commit.name())) {
List<String> circular = new ArrayList<>(IMPORT_CHAIN.get());
circular.add(commit.name());
String errorMessage = MessageFormat.format(
_T("Circular build spec imports ({0})"), circular);
@ -203,7 +203,7 @@ public class Import implements Serializable, Validatable {
context.buildConstraintViolationWithTemplate(errorMessage).addConstraintViolation();
return false;
} else {
importChain.get().push(commit.name());
IMPORT_CHAIN.get().push(commit.name());
try {
Validator validator = OneDev.getInstance(Validator.class);
BuildSpec buildSpec = getBuildSpec();
@ -226,7 +226,7 @@ public class Import implements Serializable, Validatable {
JobAuthorizationContext.pop();
}
} finally {
importChain.get().pop();
IMPORT_CHAIN.get().pop();
}
}
} catch (Exception e) {

View File

@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import io.onedev.commons.codeassist.InputCompletion;
@ -87,6 +88,7 @@ public class Service implements NamedElement {
@Editable(order=300, name="Environment Variables", description="Optionally specify environment variables of "
+ "the service")
@Valid
public List<EnvVar> getEnvVars() {
return envVars;
}

View File

@ -83,6 +83,8 @@ public class Job implements NamedElement, Validatable {
public static final String PROP_RETRY_CONDITION = "retryCondition";
public static final String PROP_POST_BUILD_ACTIONS = "postBuildActions";
public static final String PROP_JOB_EXECUTOR = "jobExecutor";
private String name;
@ -106,7 +108,7 @@ public class Job implements NamedElement, Validatable {
private String sequentialGroup;
private String retryCondition;
private String retryCondition = "never";
private int maxRetries = 3;
@ -189,6 +191,7 @@ public class Job implements NamedElement, Validatable {
}
@Editable(order=200, description="Steps will be executed serially on same node, sharing the same <a href='https://docs.onedev.io/concepts#job-workspace'>job workspace</a>")
@Valid
public List<Step> getSteps() {
return steps;
}
@ -280,8 +283,9 @@ public class Job implements NamedElement, Validatable {
this.sequentialGroup = sequentialGroup;
}
@Editable(order=9400, placeholder="Never retry", group="More Settings", description="Specify condition to retry build upon failure")
@Editable(order=9400, group="More Settings", description="Specify condition to retry build upon failure")
@RetryCondition
@NotEmpty
public String getRetryCondition() {
return retryCondition;
}
@ -292,7 +296,7 @@ public class Job implements NamedElement, Validatable {
@Editable(order=9410, group="More Settings", description="Maximum of retries before giving up")
@Min(value=1, message="This value should not be less than 1")
@DependsOn(property="retryCondition")
@DependsOn(property="retryCondition", value = "never", inverse = true)
public int getMaxRetries() {
return maxRetries;
}
@ -305,7 +309,7 @@ public class Job implements NamedElement, Validatable {
"Delay of subsequent retries will be calculated using an exponential back-off based on " +
"this value")
@Min(value=1, message="This value should not be less than 1")
@DependsOn(property="retryCondition")
@DependsOn(property="retryCondition", value = "never", inverse = true)
public int getRetryDelay() {
return retryDelay;
}
@ -348,6 +352,14 @@ public class Job implements NamedElement, Validatable {
public boolean isValid(ConstraintValidatorContext context) {
boolean isValid = true;
var jobExecutors = OneDev.getInstance(SettingManager.class).getJobExecutors();
if (jobExecutor != null && !jobExecutor.contains("@")
&& jobExecutors.stream().noneMatch(it->it.getName().equals(jobExecutor))) {
isValid = false;
context.buildConstraintViolationWithTemplate("Job executor not found: " + jobExecutor)
.addPropertyNode(PROP_JOB_EXECUTOR).addConstraintViolation();
}
Set<String> dependencyJobNames = new HashSet<>();
for (JobDependency dependency: jobDependencies) {
if (!dependencyJobNames.add(dependency.getJobName())) {
@ -366,18 +378,16 @@ public class Job implements NamedElement, Validatable {
}
}
if (getRetryCondition() != null) {
try {
io.onedev.server.buildspec.job.retrycondition.RetryCondition.parse(this, getRetryCondition());
} catch (Exception e) {
String message = e.getMessage();
if (message == null)
message = "Malformed retry condition";
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(PROP_RETRY_CONDITION)
.addConstraintViolation();
isValid = false;
}
try {
io.onedev.server.buildspec.job.retrycondition.RetryCondition.parse(this, getRetryCondition());
} catch (Exception e) {
String message = e.getMessage();
if (message == null)
message = "Malformed retry condition";
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(PROP_RETRY_CONDITION)
.addConstraintViolation();
isValid = false;
}
if (isValid) {

View File

@ -0,0 +1,31 @@
package io.onedev.server.buildspec.job.retrycondition;
import javax.annotation.Nullable;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import io.onedev.server.util.ProjectScope;
import io.onedev.server.util.criteria.Criteria;
public class NeverCriteria extends Criteria<RetryContext> {
private static final long serialVersionUID = 1L;
@Override
public Predicate getPredicate(@Nullable ProjectScope projectScope, CriteriaQuery<?> query, From<RetryContext, RetryContext> from, CriteriaBuilder builder) {
throw new UnsupportedOperationException();
}
@Override
public boolean matches(RetryContext context) {
return false;
}
@Override
public String toStringWithoutParens() {
return RetryCondition.getRuleName(RetryConditionLexer.Never);
}
}

View File

@ -1,7 +1,7 @@
grammar RetryCondition;
condition
: WS* criteria WS* EOF
: WS* (criteria|Never) WS* EOF
;
criteria
@ -12,7 +12,11 @@ criteria
| Not WS* LParens WS* criteria WS* RParens #NotCriteria
| LParens WS* criteria WS* RParens #ParensCriteria
;
Never
: 'never'
;
Contains
: 'contains'
;

View File

@ -87,57 +87,60 @@ public class RetryCondition extends Criteria<RetryContext> {
Criteria<RetryContext> criteria;
criteria = new RetryConditionBaseVisitor<Criteria<RetryContext>>() {
if (conditionContext.Never() != null) {
criteria = new NeverCriteria();
} else {
criteria = new RetryConditionBaseVisitor<Criteria<RetryContext>>() {
@Override
public Criteria<RetryContext> visitParensCriteria(ParensCriteriaContext ctx) {
return visit(ctx.criteria()).withParens(true);
}
@Override
public Criteria<RetryContext> visitFieldOperatorCriteria(FieldOperatorCriteriaContext ctx) {
String fieldName = getValue(ctx.Quoted().getText());
int operator = ctx.operator.getType();
checkField(job, fieldName, operator);
return new ParamEmptyCriteria(fieldName, operator);
}
@Override
public Criteria<RetryContext> visitFieldOperatorValueCriteria(FieldOperatorValueCriteriaContext ctx) {
String fieldName = getValue(ctx.Quoted(0).getText());
String fieldValue = getValue(ctx.Quoted(1).getText());
int operator = ctx.operator.getType();
checkField(job, fieldName, operator);
@Override
public Criteria<RetryContext> visitParensCriteria(ParensCriteriaContext ctx) {
return visit(ctx.criteria()).withParens(true);
}
@Override
public Criteria<RetryContext> visitFieldOperatorCriteria(FieldOperatorCriteriaContext ctx) {
String fieldName = getValue(ctx.Quoted().getText());
int operator = ctx.operator.getType();
checkField(job, fieldName, operator);
return new ParamEmptyCriteria(fieldName, operator);
}
@Override
public Criteria<RetryContext> visitFieldOperatorValueCriteria(FieldOperatorValueCriteriaContext ctx) {
String fieldName = getValue(ctx.Quoted(0).getText());
String fieldValue = getValue(ctx.Quoted(1).getText());
int operator = ctx.operator.getType();
checkField(job, fieldName, operator);
if (fieldName.equals(NAME_LOG))
return new LogCriteria(fieldValue);
else
return new ParamCriteria(fieldName, fieldValue, operator);
}
@Override
public Criteria<RetryContext> visitOrCriteria(OrCriteriaContext ctx) {
List<Criteria<RetryContext>> childCriterias = new ArrayList<>();
for (CriteriaContext childCtx: ctx.criteria())
childCriterias.add(visit(childCtx));
return new OrCriteria<RetryContext>(childCriterias);
}
@Override
public Criteria<RetryContext> visitAndCriteria(AndCriteriaContext ctx) {
List<Criteria<RetryContext>> childCriterias = new ArrayList<>();
for (CriteriaContext childCtx: ctx.criteria())
childCriterias.add(visit(childCtx));
return new AndCriteria<RetryContext>(childCriterias);
}
@Override
public Criteria<RetryContext> visitNotCriteria(NotCriteriaContext ctx) {
return new NotCriteria<RetryContext>(visit(ctx.criteria()));
}
}.visit(conditionContext.criteria());
if (fieldName.equals(NAME_LOG))
return new LogCriteria(fieldValue);
else
return new ParamCriteria(fieldName, fieldValue, operator);
}
@Override
public Criteria<RetryContext> visitOrCriteria(OrCriteriaContext ctx) {
List<Criteria<RetryContext>> childCriterias = new ArrayList<>();
for (CriteriaContext childCtx: ctx.criteria())
childCriterias.add(visit(childCtx));
return new OrCriteria<RetryContext>(childCriterias);
}
@Override
public Criteria<RetryContext> visitAndCriteria(AndCriteriaContext ctx) {
List<Criteria<RetryContext>> childCriterias = new ArrayList<>();
for (CriteriaContext childCtx: ctx.criteria())
childCriterias.add(visit(childCtx));
return new AndCriteria<RetryContext>(childCriterias);
}
@Override
public Criteria<RetryContext> visitNotCriteria(NotCriteriaContext ctx) {
return new NotCriteria<RetryContext>(visit(ctx.criteria()));
}
}.visit(conditionContext.criteria());
}
return new RetryCondition(criteria);
}

View File

@ -3,6 +3,8 @@ package io.onedev.server.buildspec.job.trigger;
import java.util.Collection;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@ -21,6 +23,7 @@ import io.onedev.server.event.project.RefUpdated;
import io.onedev.server.git.GitUtils;
import io.onedev.server.model.Project;
import io.onedev.server.util.patternset.PatternSet;
import io.onedev.server.util.usermatch.Anyone;
import io.onedev.server.web.util.SuggestionUtils;
@Editable(order=100, name="Branch update", description=""
@ -36,7 +39,7 @@ public class BranchUpdateTrigger extends JobTrigger {
private String paths;
private String userMatch;
private String userMatch = new Anyone().toString();
@Editable(name="Branches", order=100, placeholder="Any branch", description="Optionally specify space-separated branches "
+ "to check. Use '**' or '*' or '?' for <a href='https://docs.onedev.io/appendix/path-wildcard' target='_blank'>path wildcard match</a>. "
@ -67,8 +70,9 @@ public class BranchUpdateTrigger extends JobTrigger {
this.paths = paths;
}
@Editable(order=150, name="Applicable Users", placeholder = "Any user", description="Optionally specify applicable users who pushed the change")
@Editable(order=300, name="Applicable Users", description="Optionally specify applicable users who pushed the change")
@UserMatch
@NotEmpty
public String getUserMatch() {
return userMatch;
}
@ -108,7 +112,7 @@ public class BranchUpdateTrigger extends JobTrigger {
}
private boolean pushedBy(RefUpdated refUpdated) {
if (getUserMatch() != null && refUpdated.getUser() != null) {
if (refUpdated.getUser() != null) {
return io.onedev.server.util.usermatch.UserMatch.parse(getUserMatch()).matches(refUpdated.getUser());
} else {
return true;
@ -135,22 +139,26 @@ public class BranchUpdateTrigger extends JobTrigger {
@Override
public String getTriggerDescription() {
String description;
if (getBranches() != null && getPaths() != null && getUserMatch() != null)
description = String.format("When update branches '%s' and touch files '%s' and pushed by '%s'", getBranches(), getPaths(), getUserMatch());
else if (getBranches() != null && getUserMatch() != null)
description = String.format("When update branches '%s' and pushed by '%s'", getBranches(), getUserMatch());
else if (getPaths() != null && getUserMatch() != null)
description = String.format("When touch files '%s' and pushed by '%s'", getPaths(), getUserMatch());
else if (getBranches() != null && getPaths() != null)
description = String.format("When update branches '%s' and touch files '%s'", getBranches(), getPaths());
else if (getBranches() != null)
description = String.format("When update branches '%s'", getBranches());
else if (getPaths() != null)
description = String.format("When touch files '%s'", getPaths());
else if (getUserMatch() != null)
description = String.format("When pushed by '%s'", getUserMatch());
else
description = "When update branches";
if (getUserMatch() == null || getUserMatch().equals(new Anyone().toString())) {
if (getBranches() != null && getPaths() != null)
description = String.format("When update branches '%s' and touch files '%s'", getBranches(), getPaths());
else if (getBranches() != null)
description = String.format("When update branches '%s'", getBranches());
else if (getPaths() != null)
description = String.format("When touch files '%s'", getPaths());
else
description = "When update branches";
} else {
if (getBranches() != null && getPaths() != null)
description = String.format("When update branches '%s' and touch files '%s' and pushed by '%s'", getBranches(), getPaths(), getUserMatch());
else if (getBranches() != null)
description = String.format("When update branches '%s' and pushed by '%s'", getBranches(), getUserMatch());
else if (getPaths() != null)
description = String.format("When touch files '%s' and pushed by '%s'", getPaths(), getUserMatch());
else
description = "When pushed by '" + getUserMatch() + "'";
}
return description;
}

View File

@ -7,6 +7,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ -82,6 +83,7 @@ public class BuildImageStep extends Step {
@Editable(order=1000, group="More Settings", description="Optionally specify registry logins to override " +
"those defined in job executor. For built-in registry, use <code>@server_url@</code> for registry url, " +
"<code>@job_token@</code> for user name, and access token secret for password secret")
@Valid
public List<RegistryLogin> getRegistryLogins() {
return registryLogins;
}

View File

@ -2,6 +2,7 @@ package io.onedev.server.buildspec.step;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import io.onedev.commons.codeassist.InputSuggestion;
@ -35,6 +36,7 @@ public class CheckoutStep extends Step {
@Editable(order=100, description="By default code is cloned via an auto-generated credential, " +
"which only has read permission over current project. In case the job needs to <a href='https://docs.onedev.io/tutorials/cicd/commit-and-push' target='_blank'>push code to server</a>, " +
"you should supply custom credential with appropriate permissions here")
@Valid
@NotNull
public GitCredential getCloneCredential() {
return cloneCredential;

View File

@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@ -74,6 +75,7 @@ public class CommandStep extends Step {
}
@Editable(order=110)
@Valid
@NotNull
public Interpreter getInterpreter() {
return interpreter;
@ -100,6 +102,7 @@ public class CommandStep extends Step {
"those defined in job executor. For built-in registry, use <code>@server_url@</code> for registry url, " +
"<code>@job_token@</code> for user name, and access token secret for password secret")
@DependsOn(property="runInContainer")
@Valid
public List<RegistryLogin> getRegistryLogins() {
return registryLogins;
}
@ -110,6 +113,7 @@ public class CommandStep extends Step {
@Editable(order=9900, name="Environment Variables", group="More Settings", description="Optionally specify environment "
+ "variables for this step")
@Valid
public List<EnvVar> getEnvVars() {
return envVars;
}

View File

@ -23,7 +23,8 @@ import java.util.List;
import static io.onedev.server.buildspec.step.StepGroup.PUBLISH;
@Editable(order=1050, group= PUBLISH, name="Artifacts")
@Editable(order=1050, group= PUBLISH, name="Artifacts", description="This step copies files from job workspace " +
"to build artifacts directory, so that they can be accessed after job is completed")
public class PublishArtifactStep extends ServerSideStep {
private static final long serialVersionUID = 1L;
@ -46,8 +47,7 @@ public class PublishArtifactStep extends ServerSideStep {
this.sourcePath = sourcePath;
}
@Editable(order=100, description="Specify files under above directory to be published. "
+ "Use * or ? for pattern match")
@Editable(order=100, description="Specify files under above directory to be published")
@Interpolative(variableSuggester="suggestVariables")
@Patterns(path=true)
@NotEmpty

View File

@ -8,6 +8,7 @@ import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import io.onedev.commons.codeassist.InputSuggestion;
@ -97,6 +98,7 @@ public class RunContainerStep extends Step {
@Editable(order=400, name="Environment Variables", group="More Settings", description="Optionally specify environment "
+ "variables for the container")
@Valid
public List<EnvVar> getEnvVars() {
return envVars;
}
@ -106,6 +108,7 @@ public class RunContainerStep extends Step {
}
@Editable(order=500, group = "More Settings", description="Optionally mount directories or files under job workspace into container")
@Valid
public List<VolumeMount> getVolumeMounts() {
return volumeMounts;
}
@ -117,6 +120,7 @@ public class RunContainerStep extends Step {
@Editable(order=600, group="More Settings", description="Optionally specify registry logins to override " +
"those defined in job executor. For built-in registry, use <code>@server_url@</code> for registry url, " +
"<code>@job_token@</code> for user name, and access token secret for password secret")
@Valid
public List<RegistryLogin> getRegistryLogins() {
return registryLogins;
}

View File

@ -6,6 +6,7 @@ import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import io.onedev.commons.codeassist.InputSuggestion;
@ -46,6 +47,7 @@ public class RunImagetoolsStep extends Step {
@Editable(order=200, group="More Settings", description="Optionally specify registry logins to override " +
"those defined in job executor. For built-in registry, use <code>@server_url@</code> for registry url, " +
"<code>@job_token@</code> for user name, and access token secret for password secret")
@Valid
public List<RegistryLogin> getRegistryLogins() {
return registryLogins;
}

View File

@ -47,7 +47,10 @@ public class SetupCacheStep extends Step {
@Editable(order=100, name="Cache Key", description = "This key is used to determine if there is a cache hit in " +
"project hierarchy (search from current project to root project in order, same for load keys below). " +
"A cache is considered hit if its key is exactly the same as the key defined here")
"A cache is considered hit if its key is exactly the same as the key defined here.<br>" +
"<b>NOTE:</b> In case your project has lock files(package.json, pom.xml, etc.) able to represent cache state, " +
"this key should be defined as &lt;cache name&gt;-@file:checksum.txt@, where checksum.txt is generated " +
"from these lock files with the <b>generate checksum step</b> defined before this step")
@Interpolative(variableSuggester="suggestVariables")
@NotEmpty
public String getKey() {
@ -123,7 +126,7 @@ public class SetupCacheStep extends Step {
@SuppressWarnings("unused")
private static List<InputSuggestion> suggestVariables(String matchWith) {
return BuildSpec.suggestVariables(matchWith, true, false, false);
return BuildSpec.suggestVariables(matchWith, true, true, false);
}
@Editable(order=500, description = "Specify a <a href='https://docs.onedev.io/tutorials/cicd/job-secrets' target='_blank'>job secret</a> whose value is an access token with upload cache permission " +

View File

@ -36,7 +36,7 @@ public abstract class Step implements Serializable {
this.name = name;
}
@Editable(order=10000, description="Under which condition this step should run. <b>Successful</b> means all " +
@Editable(order=10000, description="Under which condition this step should run. <b>SUCCESSFUL</b> means all " +
"non-optional steps running before this step are successful")
@NotNull
public ExecuteCondition getCondition() {

View File

@ -51,6 +51,7 @@ public class StepTemplate implements NamedElement {
}
@Editable(order=200, description="Steps will be executed serially on same node, sharing the same <a href='https://docs.onedev.io/concepts#job-workspace'>job workspace</a>")
@Valid
public List<Step> getSteps() {
return steps;
}

View File

@ -68,6 +68,7 @@ public class UseTemplateStep extends CompositeStep {
@Editable(order=300, name="Exclude Param Combos")
@ShowCondition("isExcludeParamMapsVisible")
@Valid
public List<ParamMap> getExcludeParamMaps() {
return excludeParamMaps;
}

View File

@ -8304,4 +8304,27 @@ public class DataMigrator {
}
}
private void migrate211(File dataDir, Stack<Integer> versions) {
for (File file : dataDir.listFiles()) {
if (file.getName().startsWith("Projects.xml")) {
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
for (Element projectElement : dom.getRootElement().elements()) {
for (Element branchProtectionElement : projectElement.element("branchProtections").elements()) {
Element userMatchElement = branchProtectionElement.element("userMatch");
if (userMatchElement == null) {
branchProtectionElement.addElement("userMatch").setText("anyone");
}
}
for (Element tagProtectionElement : projectElement.element("tagProtections").elements()) {
Element userMatchElement = tagProtectionElement.element("userMatch");
if (userMatchElement == null) {
tagProtectionElement.addElement("userMatch").setText("anyone");
}
}
}
dom.writeToFile(file, false);
}
}
}
}

View File

@ -703,23 +703,19 @@ public class DefaultJobManager implements JobManager, Runnable, CodePullAuthoriz
private boolean checkRetry(Job job, JobContext jobContext, TaskLogger jobLogger,
@Nullable Throwable throwable, int retried) {
if (retried < job.getMaxRetries() && sessionManager.call(() -> {
if (job.getRetryCondition() != null) {
RetryCondition retryCondition = RetryCondition.parse(job, job.getRetryCondition());
AtomicReference<String> errorMessage = new AtomicReference<>(null);
if (throwable != null) {
log(throwable, new TaskLogger() {
RetryCondition retryCondition = RetryCondition.parse(job, job.getRetryCondition());
AtomicReference<String> errorMessage = new AtomicReference<>(null);
if (throwable != null) {
log(throwable, new TaskLogger() {
@Override
public void log(String message, String sessionId) {
errorMessage.set(message);
}
@Override
public void log(String message, String sessionId) {
errorMessage.set(message);
}
});
}
return retryCondition.matches(new RetryContext(buildManager.load(jobContext.getBuildId()), errorMessage.get()));
} else {
return false;
});
}
return retryCondition.matches(new RetryContext(buildManager.load(jobContext.getBuildId()), errorMessage.get()));
})) {
if (throwable != null)
log(throwable, jobLogger);

View File

@ -1342,7 +1342,7 @@ public class Project extends AbstractEntity implements LabelSupport<ProjectLabel
public TagProtection getTagProtection(String tagName, User user) {
for (TagProtection protection: getHierarchyTagProtections()) {
if (protection.isEnabled()
&& (protection.getUserMatch() == null || UserMatch.parse(protection.getUserMatch()).matches(user))
&& UserMatch.parse(protection.getUserMatch()).matches(user)
&& PatternSet.parse(protection.getTags()).matches(new PathMatcher(), tagName)) {
return protection;
}
@ -1361,7 +1361,7 @@ public class Project extends AbstractEntity implements LabelSupport<ProjectLabel
branchName = "main";
for (BranchProtection protection: getHierarchyBranchProtections()) {
if (protection.isEnabled()
&& (protection.getUserMatch() == null || UserMatch.parse(protection.getUserMatch()).matches(user))
&& UserMatch.parse(protection.getUserMatch()).matches(user)
&& PatternSet.parse(protection.getBranches()).matches(new PathMatcher(), branchName)) {
return protection;
}

View File

@ -41,6 +41,7 @@ import io.onedev.server.model.User;
import io.onedev.server.util.patternset.PatternSet;
import io.onedev.server.util.reviewrequirement.ReviewRequirement;
import io.onedev.server.util.usage.Usage;
import io.onedev.server.util.usermatch.Anyone;
import io.onedev.server.util.usermatch.UserMatch;
import io.onedev.server.web.util.SuggestionUtils;
@ -57,7 +58,7 @@ public class BranchProtection implements Serializable {
private String branches;
private String userMatch;
private String userMatch = new Anyone().toString();
private boolean preventForcedPush = true;
@ -116,8 +117,9 @@ public class BranchProtection implements Serializable {
return SuggestionUtils.suggestBranches(Project.get(), matchWith);
}
@Editable(order=150, name="Applicable Users", placeholder = "Any user", description="Rule will apply only if the user changing the branch matches criteria specified here")
@Editable(order=150, name="Applicable Users", description="Rule will apply only if the user changing the branch matches criteria specified here")
@io.onedev.server.annotation.UserMatch
@NotEmpty(message="may not be empty")
public String getUserMatch() {
return userMatch;
}
@ -309,8 +311,7 @@ public class BranchProtection implements Serializable {
}
public void onRenameGroup(String oldName, String newName) {
if (userMatch != null)
userMatch = UserMatch.onRenameGroup(userMatch, oldName, newName);
userMatch = UserMatch.onRenameGroup(userMatch, oldName, newName);
reviewRequirement = ReviewRequirement.onRenameGroup(reviewRequirement, oldName, newName);
for (FileProtection fileProtection: getFileProtections()) {
@ -321,7 +322,7 @@ public class BranchProtection implements Serializable {
public Usage onDeleteGroup(String groupName) {
Usage usage = new Usage();
if (userMatch != null && UserMatch.isUsingGroup(userMatch, groupName))
if (UserMatch.isUsingGroup(userMatch, groupName))
usage.add("applicable users");
if (ReviewRequirement.isUsingGroup(reviewRequirement, groupName))
usage.add("required reviewers");
@ -336,8 +337,7 @@ public class BranchProtection implements Serializable {
}
public void onRenameUser(String oldName, String newName) {
if (userMatch != null)
userMatch = UserMatch.onRenameUser(userMatch, oldName, newName);
userMatch = UserMatch.onRenameUser(userMatch, oldName, newName);
reviewRequirement = ReviewRequirement.onRenameUser(reviewRequirement, oldName, newName);
for (FileProtection fileProtection: getFileProtections()) {
@ -348,7 +348,7 @@ public class BranchProtection implements Serializable {
public Usage onDeleteUser(String userName) {
Usage usage = new Usage();
if (userMatch != null && UserMatch.isUsingUser(userMatch, userName))
if (UserMatch.isUsingUser(userMatch, userName))
usage.add("applicable users");
if (ReviewRequirement.isUsingUser(reviewRequirement, userName))
usage.add("required reviewers");

View File

@ -1,20 +1,20 @@
package io.onedev.server.model.support.code;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import io.onedev.commons.codeassist.InputSuggestion;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.Patterns;
import io.onedev.server.model.Project;
import io.onedev.server.util.patternset.PatternSet;
import io.onedev.server.util.usage.Usage;
import io.onedev.server.util.usermatch.Anyone;
import io.onedev.server.util.usermatch.UserMatch;
import io.onedev.server.web.util.SuggestionUtils;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Editable
public class TagProtection implements Serializable {
@ -24,7 +24,7 @@ public class TagProtection implements Serializable {
private String tags;
private String userMatch;
private String userMatch = new Anyone().toString();
private boolean preventUpdate = true;
@ -59,8 +59,9 @@ public class TagProtection implements Serializable {
return SuggestionUtils.suggestTags(Project.get(), matchWith);
}
@Editable(order=150, name="Applicable Users", placeholder = "Any user", description="Rule will apply if user operating the tag matches criteria specified here")
@Editable(order=150, name="Applicable Users", description="Rule will apply if user operating the tag matches criteria specified here")
@io.onedev.server.annotation.UserMatch
@NotEmpty(message="may not be empty")
public String getUserMatch() {
return userMatch;
}
@ -115,25 +116,23 @@ public class TagProtection implements Serializable {
}
public void onRenameGroup(String oldName, String newName) {
if (userMatch != null)
userMatch = UserMatch.onRenameGroup(userMatch, oldName, newName);
userMatch = UserMatch.onRenameGroup(userMatch, oldName, newName);
}
public Usage onDeleteGroup(String groupName) {
Usage usage = new Usage();
if (userMatch != null && UserMatch.isUsingGroup(userMatch, groupName))
if (UserMatch.isUsingGroup(userMatch, groupName))
usage.add("applicable users");
return usage.prefix("tag protection '" + getTags() + "'").prefix("code");
}
public void onRenameUser(String oldName, String newName) {
if (userMatch != null)
userMatch = UserMatch.onRenameUser(userMatch, oldName, newName);
userMatch = UserMatch.onRenameUser(userMatch, oldName, newName);
}
public Usage onDeleteUser(String userName) {
Usage usage = new Usage();
if (userMatch != null && UserMatch.isUsingUser(userMatch, userName))
if (UserMatch.isUsingUser(userMatch, userName))
usage.add("applicable users");
return usage.prefix("tag protection '" + getTags() + "'").prefix("code");
}

View File

@ -20,6 +20,7 @@ import javax.inject.Singleton;
import javax.persistence.EntityNotFoundException;
import javax.validation.Valid;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@ -30,6 +31,7 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
@ -38,8 +40,11 @@ import org.unbescape.html.HtmlEscape;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.SubscriptionManager;
import io.onedev.server.buildspec.BuildSpec;
@ -66,6 +71,7 @@ import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.entityreference.BuildReference;
import io.onedev.server.entityreference.IssueReference;
import io.onedev.server.entityreference.PullRequestReference;
import io.onedev.server.exception.ExceptionUtils;
import io.onedev.server.exception.InvalidIssueFieldsException;
import io.onedev.server.exception.InvalidReferenceException;
import io.onedev.server.exception.IssueLinkValidationException;
@ -171,6 +177,8 @@ public class McpHelperResource {
private final UrlManager urlManager;
private final Validator validator;
@Inject
public McpHelperResource(ObjectMapper objectMapper, SettingManager settingManager,
UserManager userManager, IssueManager issueManager, ProjectManager projectManager,
@ -183,7 +191,7 @@ public class McpHelperResource {
PullRequestAssignmentManager pullRequestAssignmentManager,
PullRequestLabelManager pullRequestLabelManager, UrlManager urlManager,
PullRequestCommentManager pullRequestCommentManager, BuildManager buildManager,
BuildParamManager buildParamManager, JobManager jobManager) {
BuildParamManager buildParamManager, JobManager jobManager, Validator validator) {
this.objectMapper = objectMapper;
this.settingManager = settingManager;
this.issueManager = issueManager;
@ -208,6 +216,7 @@ public class McpHelperResource {
this.buildManager = buildManager;
this.buildParamManager = buildParamManager;
this.jobManager = jobManager;
this.validator = validator;
}
private String getIssueQueryStringDescription() {
@ -1464,18 +1473,6 @@ public class McpHelperResource {
}
}
@Path("/migrate-build-spec")
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public String migrateBuildSpec(@NotNull String buildSpecString) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var buildSpec = BuildSpec.parse(buildSpecString.getBytes(StandardCharsets.UTF_8));
return VersionedYamlDoc.fromBean(buildSpec).toYaml();
}
@Path("/get-pull-request")
@GET
public Map<String, Object> getPullRequest(
@ -1678,7 +1675,16 @@ public class McpHelperResource {
if (baseCommitId == null)
throw new NotAcceptableException("No common base for source and target branches");
request.setTitle((String) data.remove("title"));
var title = (String) data.remove("title");
// Remove issue number suffix generated by AI
var cleanedTitle = title.replaceFirst("\\s*\\([a-zA-Z]+-\\d+\\)$", "");
cleanedTitle = cleanedTitle.replaceFirst("\\s*\\(#\\d+\\)$", "").trim();
if (cleanedTitle.length() != 0)
request.setTitle(cleanedTitle);
else
request.setTitle(title);
request.setDescription((String) data.remove("description"));
request.setTarget(target);
request.setSource(source);
@ -2125,6 +2131,43 @@ public class McpHelperResource {
return buildMap;
}
@Path("/check-build-spec")
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public Response checkBuildSpec(
@QueryParam("project") @NotNull String projectPath,
@NotNull String buildSpecString) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var project = getProject(projectPath);
Project.push(project);
try {
var buildSpec = BuildSpec.parse(buildSpecString.getBytes(StandardCharsets.UTF_8));
List<String> validationErrors = new ArrayList<>();
for (var violation : validator.validate(buildSpec)) {
String message = String.format("Error validating build spec (project: %s, location: %s, message: %s)",
project.getPath(), violation.getPropertyPath(), violation.getMessage());
validationErrors.add(message);
}
if (validationErrors.isEmpty()) {
return Response.ok(VersionedYamlDoc.fromBean(buildSpec).toYaml()).build();
} else {
return Response.ok(Joiner.on("\n").join(validationErrors)).build();
}
} catch (Exception e) {
var explicitException = ExceptionUtils.find(e, ExplicitException.class);
if (explicitException != null) {
return Response.ok(explicitException.getMessage()).build();
} else {
return Response.ok(Throwables.getStackTraceAsString(e)).build();
}
} finally {
Project.pop();
}
}
private Build getBuild(Project currentProject, String referenceString) {
BuildReference buildReference;
try {

View File

@ -0,0 +1,32 @@
package io.onedev.server.util;
import io.onedev.server.annotation.DependsOn;
public class DependsOnUtils {
public static boolean isPropertyVisible(DependsOn dependsOn, Class<?> dependencyPropertyType, Object dependencyPropertyValue) {
if (dependsOn.value().length() != 0) {
if (dependencyPropertyValue != null && dependencyPropertyValue.toString().equals(dependsOn.value())) {
if (dependsOn.inverse())
return false;
} else if (!dependsOn.inverse()) {
return false;
}
} else {
if (dependencyPropertyType == boolean.class) {
boolean requiredPropertyValue = !dependsOn.inverse();
if (requiredPropertyValue != (boolean)dependencyPropertyValue)
return false;
} else if (dependencyPropertyType == int.class || dependencyPropertyType == long.class || dependencyPropertyType == double.class || dependencyPropertyType == float.class) {
int dependencyPropertyIntValue = (int) dependencyPropertyValue;
if (dependsOn.inverse() && dependencyPropertyIntValue != 0 || !dependsOn.inverse() && dependencyPropertyIntValue == 0)
return false;
} else {
if (dependsOn.inverse() && dependencyPropertyValue != null || !dependsOn.inverse() && dependencyPropertyValue == null)
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,24 @@
package io.onedev.server.validation;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import io.onedev.server.annotation.Shallow;
public class ShallowValidatorProvider implements Provider<Validator> {
private final ValidatorFactory validatorFactory;
@Inject
public ShallowValidatorProvider(@Shallow ValidatorFactory validatorFactory) {
this.validatorFactory = validatorFactory;
}
@Override
public Validator get() {
return validatorFactory.getValidator();
}
}

View File

@ -21,6 +21,7 @@ public class RegExValidator implements ConstraintValidator<RegEx, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintContext) {
System.out.println("Validating value: " + value);
if (value == null)
return true;
if (pattern.matcher(value).matches()) {

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4f, 2020-05-01)"
sodipodi:docname="edit2.svg"
id="svg45"
version="1.1"
viewBox="0 0 1024 1024"
height="200.00px"
width="200px"
class="icon">
<metadata
id="metadata51">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs49" />
<sodipodi:namedview
inkscape:current-layer="svg45"
inkscape:window-maximized="1"
inkscape:window-y="23"
inkscape:window-x="0"
inkscape:cy="162.63603"
inkscape:cx="100"
inkscape:zoom="4.135"
showgrid="false"
id="namedview47"
inkscape:window-height="971"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<path
style="stroke-width:0.872879"
id="path43"
d="m 851.96996,76.740834 c 34.21793,13.273184 63.02429,32.821286 79.26775,67.228716 7.05515,14.9465 11.36518,30.37233 11.36518,47.06185 -0.0962,57.73792 0.20107,115.47583 -0.0437,173.21375 -0.0962,23.13003 -13.23607,37.7802 -32.01484,37.0917 -18.61266,-0.67977 -31.87495,-15.89644 -32.00608,-38.34669 -0.29725,-51.07953 0.2273,-102.15907 -0.0962,-153.22989 -0.31473,-50.16443 -20.10762,-69.50338 -70.61273,-69.5208 q -299.62105,-0.0959 -599.25085,0 c -49.10632,0.0436 -68.71562,19.88798 -68.71562,68.72773 v 624.00523 c 0,48.85717 19.61805,68.51856 68.72436,68.63186 55.68937,0.13073 111.38748,-0.14816 167.07686,0.15687 23.69202,0.13073 38.63286,11.59988 40.41631,29.63153 1.89712,19.17334 -12.19571,34.75607 -34.20918,34.92161 -66.08414,0.48805 -132.21199,1.51649 -198.24367,-0.47055 C 125.77007,964.10064 75.876939,909.97956 75.343649,850.49861 74.609284,763.93967 75.098862,677.32844 75.081377,590.75207 75.063892,466.36064 76.91729,341.96049 74.347011,217.6562 72.887022,147.73451 100.75794,101.46573 165.76675,76.740834 Z M 969.36349,566.71567 c 0.17486,-42.62583 -32.5481,-89.85327 -73.17424,-106.62123 -42.94289,-17.72663 -78.682,-7.31201 -110.88043,25.38725 -74.80035,76.03103 -148.69149,153.1166 -226.73529,225.72257 -50.32151,46.87883 -69.01286,105.64513 -80.98127,168.4204 -8.13921,42.59098 21.34031,79.05516 63.07674,74.50588 55.50578,-6.05701 109.75265,-21.2214 150.94705,-62.99315 83.18436,-84.38887 165.54693,-169.57081 248.47775,-254.19499 19.6618,-20.04486 30.90459,-43.08773 29.26969,-70.2093 z M 546.9111,905.43025 c -16.61064,1.48157 -20.38737,-5.41212 -17.96572,-16.08819 11.53129,-50.81808 26.22733,-99.71882 65.11373,-138.33568 54.70148,-54.28671 108.15278,-109.81098 161.24564,-165.70128 11.73237,-12.34938 18.8662,-12.46267 30.02156,-0.18302 17.92201,19.73112 37.01551,38.43385 56.1702,57.01456 8.86484,8.60186 8.81239,15.06851 0.13989,24.01897 -61.37196,63.32433 -121.94837,127.40688 -183.59134,190.42618 -31.23675,31.90619 -75.40359,36.01103 -111.09024,48.84846 z M 918.465,570.19302 c -0.31473,8.07895 0,16.04461 -4.9832,23.40019 -14.05784,20.62878 -21.65503,21.66589 -39.25358,4.23557 -17.89577,-17.72663 -35.26702,-36.01974 -53.70483,-53.16246 -10.21992,-9.50823 -8.92603,-15.33867 0.64694,-24.89049 16.41832,-16.38449 34.34033,-19.26921 54.83261,-11.25998 26.49836,10.3449 42.52325,34.07627 42.46206,61.68589 z M 502.82296,240.23718 q -96.85754,0 -193.71508,0.0784 c -8.113,0 -16.50575,0 -24.26903,2.00449 -15.86753,4.0264 -26.29728,14.0924 -26.40218,31.31356 -0.10492,17.22114 9.50304,27.69676 25.86889,31.37456 a 120.64573,120.26917 0 0 0 26.42841,2.74527 q 191.45953,0.25274 382.98901,0.14816 c 11.19906,0 22.42437,-0.18302 32.86284,-4.50573 12.82517,-5.31625 19.77541,-15.08595 19.77541,-29.30036 0,-14.03141 -6.25084,-23.9754 -19.23337,-29.69255 -9.80902,-4.35757 -20.20379,-4.21813 -30.59856,-4.20941 H 502.81422 Z m -67.46544,233.04329 c 48.24081,0 96.49035,0.39219 144.72242,-0.22659 20.91193,-0.27017 35.6954,-14.33644 36.12378,-31.80161 0.45461,-18.65043 -14.40755,-32.09792 -36.91934,-32.78642 -14.08408,-0.43576 -28.20313,-0.13944 -42.30469,-0.13944 -80.15948,0 -160.31021,-0.19173 -240.46968,0.16558 -26.91799,0.12202 -43.71223,18.51972 -36.98054,40.00258 5.62139,17.89222 19.53062,24.8992 37.7761,24.84691 46.01148,-0.11329 92.03172,0 138.0432,-0.0697 z m -63.45267,169.07405 c 28.16816,0 56.33631,0.2876 84.49573,-0.0697 19.23338,-0.24401 30.51987,-10.54533 32.48692,-29.52694 a 30.782147,30.686068 0 0 0 -30.43245,-34.11112 q -84.45202,-1.6646 -168.95648,-0.12202 c -19.56559,0.33989 -31.90118,15.49555 -31.40286,33.40519 0.50706,18.30183 12.76396,29.86685 33.75458,30.27646 26.6732,0.52291 53.32891,0.13073 80.04582,0.14816 z" />
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1758698793059" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1512" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M354.40128 0c-87.04 0-157.44 70.55872-157.44 157.59872v275.68128H78.72c-21.6576 0-39.36256 17.69984-39.36256 39.36256v236.31872c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.24128v118.08256c0 87.04 70.4 157.59872 157.44 157.59872h472.63744c87.04 0 157.59872-70.55872 157.59872-157.59872V315.0336c0-41.74848-38.9888-81.93024-107.52-149.27872l-29.11744-29.12256L818.87744 107.52C751.5392 38.9888 711.39328 0 669.59872 0H354.4064z m0 78.72h287.20128c28.35456 7.0912 27.99616 42.1376 27.99616 76.8v120.16128c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.07744c39.38816 0 78.87872-0.0256 78.87872 39.36256v512c0 43.32032-35.55328 78.87872-78.87872 78.87872H354.4064c-43.32544 0-78.72-35.5584-78.72-78.87872v-118.08256h393.91744c21.66272 0 39.36256-17.69472 39.36256-39.35744V472.64256c0-21.66272-17.69984-39.36256-39.36256-39.36256H275.68128V157.59872c0-43.32032 35.39456-78.87872 78.72-78.87872zM261.2736 506.39872h20.16256l65.28 176.64h-23.04l-19.2-54.71744h-65.28l-19.2 54.71744h-23.04l64.31744-176.64z m-181.43744 0.96256h23.99744l40.32 89.27744 41.28256-89.27744h23.99744l-53.76 107.52v68.15744h-22.07744v-67.2l-53.76-108.47744z m290.87744 0h32.64l49.92 143.03744h0.96256l48.95744-143.03744h33.60256v175.67744h-22.08256v-106.55744c0-10.88 0.32256-26.56256 0.96256-47.04256h-0.96256l-52.79744 153.6h-19.2l-52.80256-153.6h-0.95744c1.28 22.4 1.92 38.72256 1.92 48.96256v104.63744h-20.16256V507.36128z m214.08256 0h22.07744v155.52h69.12v20.15744H584.8064V507.36128z m-312.96 23.04c-1.92 8.96-4.80256 18.23744-8.64256 27.83744l-17.28 50.88256h51.84l-18.23744-50.88256c-3.84-10.88-6.4-20.15744-7.68-27.83744z" p-id="1513"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -34,6 +34,7 @@ import org.unbescape.html.HtmlEscape;
import io.onedev.commons.loader.AppLoader;
import io.onedev.server.annotation.OmitName;
import io.onedev.server.annotation.Shallow;
import io.onedev.server.annotation.SubscriptionRequired;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.util.EditContext;
@ -294,7 +295,7 @@ public class BeanEditor extends ValueEditor<Serializable> {
add(validatable -> {
ComponentContext.push(newComponentContext());
try {
Validator validator = AppLoader.getInstance(Validator.class);
Validator validator = AppLoader.getInstance(Validator.class, Shallow.class);
for (var violation : validator.validate(validatable.getValue()))
error(new Path(violation.getPropertyPath()), violation.getMessage());
} finally {

View File

@ -21,6 +21,7 @@ import io.onedev.server.annotation.ShowCondition;
import io.onedev.server.annotation.SubscriptionRequired;
import io.onedev.server.util.BeanUtils;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.util.DependsOnUtils;
import io.onedev.server.util.EditContext;
import io.onedev.server.util.ReflectionUtils;
@ -175,28 +176,9 @@ public class PropertyDescriptor implements Serializable {
if (dependencyProperty == null) {
throw new ExplicitException("Dependency property not found: " + dependsOn.property());
}
var dependencyPropertyValue = EditContext.get().getInputValue(dependsOn.property());
if (dependsOn.value().length() != 0) {
if (dependencyPropertyValue != null && dependencyPropertyValue.toString().equals(dependsOn.value())) {
if (dependsOn.inverse())
return false;
} else if (!dependsOn.inverse()) {
return false;
}
} else {
if (dependencyProperty.getPropertyClass() == boolean.class) {
boolean requiredPropertyValue = !dependsOn.inverse();
if (requiredPropertyValue != (boolean)dependencyPropertyValue)
return false;
} else if (dependencyProperty.getPropertyClass() == int.class || dependencyProperty.getPropertyClass() == long.class || dependencyProperty.getPropertyClass() == double.class || dependencyProperty.getPropertyClass() == float.class) {
int dependencyPropertyIntValue = (int) dependencyPropertyValue;
if (dependsOn.inverse() && dependencyPropertyIntValue != 0 || !dependsOn.inverse() && dependencyPropertyIntValue == 0)
return false;
} else {
if (dependsOn.inverse() && dependencyPropertyValue != null || !dependsOn.inverse() && dependencyPropertyValue == null)
return false;
}
}
var dependencyPropertyValue = EditContext.get().getInputValue(dependsOn.property());
if (!DependsOnUtils.isPropertyVisible(dependsOn, dependencyProperty.getPropertyClass(), dependencyPropertyValue))
return false;
}
getDependencyPropertyNames().remove(getPropertyName());
for (String dependencyPropertyName: getDependencyPropertyNames()) {

View File

@ -1,5 +1,40 @@
package io.onedev.server.web.editable.buildspec.step;
import static io.onedev.server.web.component.floating.AlignPlacement.bottom;
import static io.onedev.server.web.translation.Translation._T;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
import org.apache.wicket.extensions.markup.html.repeater.data.table.HeadersToolbar;
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.NoRecordsToolbar;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.IDataProvider;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.convert.ConversionException;
import org.unbescape.html.HtmlEscape;
import io.onedev.server.buildspec.BuildSpec;
import io.onedev.server.buildspec.BuildSpecAware;
import io.onedev.server.buildspec.ParamSpecAware;
@ -20,35 +55,6 @@ import io.onedev.server.web.editable.PropertyEditor;
import io.onedev.server.web.editable.PropertyUpdating;
import io.onedev.server.web.util.TextUtils;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.event.IEvent;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.*;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.IDataProvider;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.convert.ConversionException;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import static io.onedev.server.web.component.floating.AlignPlacement.bottom;
import static io.onedev.server.web.translation.Translation._T;
class StepListEditPanel extends PropertyEditor<List<Serializable>> {
private final List<Step> steps;
@ -112,7 +118,11 @@ class StepListEditPanel extends PropertyEditor<List<Serializable>> {
@Override
public void populateItem(Item<ICellPopulator<Step>> cellItem, String componentId, IModel<Step> rowModel) {
cellItem.add(new Label(componentId, _T(TextUtils.getDisplayValue(rowModel.getObject().getCondition()))));
var condition = rowModel.getObject().getCondition();
if (condition != null)
cellItem.add(new Label(componentId, _T(TextUtils.getDisplayValue(condition))));
else
cellItem.add(new Label(componentId, "<i>" + HtmlEscape.escapeHtml5(_T("Unspecified")) + "</i>").setEscapeModelStrings(false));
}
});

View File

@ -224,14 +224,14 @@ onedev.server = {
setTimeout(function() {
// do not use :visible selector directly for performance reason
var focusibleSelector = "input[type=text], input[type=password], input:not([type]), textarea, .CodeMirror";
var attentionSelector = ".is-invalid";
var attentionSelector = ".feedbackPanelERROR";
var $attention = $containers.find(attentionSelector).addBack(attentionSelector).filter(":visible:first");
if ($attention.length == 0) {
attentionSelector = ".feedbackPanelERROR";
attentionSelector = ".feedbackPanelWARNING";
$attention = $containers.find(attentionSelector).addBack(attentionSelector).filter(":visible:first");
}
if ($attention.length == 0) {
attentionSelector = ".feedbackPanelWARNING";
attentionSelector = ".is-invalid";
$attention = $containers.find(attentionSelector).addBack(attentionSelector).filter(":visible:first");
}

View File

@ -9,8 +9,8 @@
</div>
<div wicket:id="editPlainTab" class="tab edit-plain mr-4 d-flex align-items-stretch">
<a class="d-flex align-items-center font-weight-bold">
<wicket:svg href="edit-file" class="icon mr-1"></wicket:svg>
<wicket:t>Edit Source</wicket:t>
<wicket:svg href="yaml" class="icon mr-1"></wicket:svg>
<wicket:t>YAML</wicket:t>
</a>
</div>
<div wicket:id="saveTab" class="save tab mr-4 d-flex align-items-stretch">

View File

@ -17,7 +17,6 @@ public class TestPage extends BasePage {
@Override
protected void onInitialize() {
super.onInitialize();
add(new Link<Void>("test") {
@Override

View File

@ -11,6 +11,7 @@ import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@ -23,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.ConstraintViolation;
@ -66,7 +68,9 @@ import org.hibernate.validator.internal.metadata.aggregated.ReturnValueMetaData;
import org.hibernate.validator.internal.metadata.core.MetaConstraint;
import org.hibernate.validator.internal.metadata.facets.Cascadable;
import org.hibernate.validator.internal.metadata.facets.Validatable;
import org.hibernate.validator.internal.metadata.location.AbstractPropertyConstraintLocation;
import org.hibernate.validator.internal.metadata.location.ConstraintLocation.ConstraintLocationKind;
import org.hibernate.validator.internal.metadata.location.TypeArgumentConstraintLocation;
import org.hibernate.validator.internal.util.Contracts;
import org.hibernate.validator.internal.util.ExecutableHelper;
import org.hibernate.validator.internal.util.ReflectionHelper;
@ -74,7 +78,14 @@ import org.hibernate.validator.internal.util.TypeHelper;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.annotation.ClassValidating;
import io.onedev.server.annotation.DependsOn;
import io.onedev.server.annotation.ShowCondition;
import io.onedev.server.util.BeanUtils;
import io.onedev.server.util.DependsOnUtils;
import io.onedev.server.util.EditContext;
import io.onedev.server.util.ReflectionUtils;
/**
* The main Bean Validation class. This is the core processing class of Hibernate Validator.
@ -401,7 +412,7 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
while ( groupIterator.hasNext() ) {
Group group = groupIterator.next();
valueContext.setCurrentGroup( group.getDefiningClass() );
validateConstraintsForCurrentGroup( validationContext, valueContext );
validatePropertyConstraintsForCurrentGroup( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
@ -416,6 +427,20 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
}
}
/*
* Validate class constraints after cascading constraints. This ensures that when class validator
* is called, all properties have been validated recursively
*/
groupIterator = validationOrder.getGroupIterator();
while ( groupIterator.hasNext() ) {
Group group = groupIterator.next();
valueContext.setCurrentGroup( group.getDefiningClass() );
validateClassConstraintsForCurrentGroup( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
}
// now we process sequences. For sequences I have to traverse the object graph since I have to stop processing when an error occurs.
Iterator<Sequence> sequenceIterator = validationOrder.getSequenceIterator();
while ( sequenceIterator.hasNext() ) {
@ -426,7 +451,7 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
for ( Group group : groupOfGroups ) {
valueContext.setCurrentGroup( group.getDefiningClass() );
validateConstraintsForCurrentGroup( validationContext, valueContext );
validatePropertyConstraintsForCurrentGroup( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
@ -435,6 +460,15 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
/*
* Validate class constraints after cascading constraints. This ensures that when class validator
* is called, all properties have been validated recursively
*/
validateClassConstraintsForCurrentGroup( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
}
if ( validationContext.getFailingConstraints().size() > numberOfViolations ) {
break;
@ -607,14 +641,14 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
CascadingMetaData effectiveCascadingMetaData = cascadingMetaData.addRuntimeContainerSupport( valueExtractorManager, value.getClass() );
// validate cascading on the annotated object
if ( effectiveCascadingMetaData.isCascading() ) {
if ( effectiveCascadingMetaData.isCascading() && isCascadeValidationRequired( validationContext, valueContext, cascadable ) ) {
validateCascadedAnnotatedObjectForCurrentGroup( value, validationContext, valueContext, effectiveCascadingMetaData );
}
if ( effectiveCascadingMetaData.isContainer() ) {
ContainerCascadingMetaData containerCascadingMetaData = effectiveCascadingMetaData.as( ContainerCascadingMetaData.class );
if ( containerCascadingMetaData.hasContainerElementsMarkedForCascading() ) {
if ( containerCascadingMetaData.hasContainerElementsMarkedForCascading() && isCascadeValidationRequired( validationContext, valueContext, cascadable ) ) {
// validate cascading on the container elements
validateCascadedContainerElementsForCurrentGroup( value, validationContext, valueContext,
containerCascadingMetaData.getContainerElementTypesCascadingMetaData() );
@ -1279,6 +1313,56 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
private boolean isValidationRequired(BaseBeanValidationContext<?> validationContext,
ValueContext<?, ?> valueContext,
MetaConstraint<?> metaConstraint) {
var location = metaConstraint.getLocation();
if (location instanceof TypeArgumentConstraintLocation) {
location = ((TypeArgumentConstraintLocation) location).getOuterDelegate();
}
if (location instanceof AbstractPropertyConstraintLocation && valueContext.getCurrentBean() != null) {
var bean = valueContext.getCurrentBean();
var propertyName = ((AbstractPropertyConstraintLocation<?>) location).getPropertyName();
var getter = BeanUtils.findGetter(bean.getClass(), propertyName);
if (getter == null) {
throw new ExplicitException("Getter not found for property: " + propertyName);
}
var dependsOn = getter.getAnnotation(DependsOn.class);
if (dependsOn != null) {
var dependencyGetter = BeanUtils.findGetter(bean.getClass(), dependsOn.property());
if (dependencyGetter == null) {
throw new ExplicitException("Getter not found for property: " + dependsOn.property());
}
try {
var dependencyPropertyValue = dependencyGetter.invoke(bean);
if (!DependsOnUtils.isPropertyVisible(dependsOn, dependencyGetter.getReturnType(), dependencyPropertyValue))
return false;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new ExplicitException("Error invoking getter for property: " + dependsOn.property(), e);
}
}
var showCondition = getter.getAnnotation(ShowCondition.class);
if (showCondition != null) {
EditContext.push(new EditContext() {
@Override
public Object getInputValue(String name) {
var getter = BeanUtils.findGetter(bean.getClass(), name);
if (getter == null) {
throw new ExplicitException("Getter not found for property: " + name);
}
try {
return getter.invoke(bean);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new ExplicitException("Error invoking getter for property: " + dependsOn.property(), e);
}
}
});
try {
if (!(boolean)ReflectionUtils.invokeStaticMethod(bean.getClass(), showCondition.value()))
return false;
} finally {
EditContext.pop();
}
}
}
// check if this validation context is qualified to validate the current meta constraint.
// For instance, in the case of validateProperty()/validateValue(), the current meta constraint
// could be for another property and, in this case, we don't validate it.
@ -1336,6 +1420,59 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
|| isReturnValueValidation( path );
}
private boolean isCascadeValidationRequired(BaseBeanValidationContext<?> validationContext, ValueContext<?, Object> valueContext, Cascadable cascadable) {
if (valueContext.getCurrentBean() != null && cascadable.getConstraintLocationKind() == ConstraintLocationKind.GETTER) {
var bean = valueContext.getCurrentBean();
var currentPath = valueContext.getPropertyPath();
var leafNode = currentPath.getLeafNode();
if (leafNode != null) {
var propertyName = leafNode.getName();
Method getter = BeanUtils.findGetter(bean.getClass(), propertyName);
if (getter == null) {
throw new ExplicitException("Getter not found for property: " + propertyName);
}
DependsOn dependsOn = getter.getAnnotation(DependsOn.class);
if (dependsOn != null) {
Method dependencyGetter = BeanUtils.findGetter(bean.getClass(), dependsOn.property());
if (dependencyGetter == null) {
throw new ExplicitException("Getter not found for property: " + dependsOn.property());
}
try {
var dependencyPropertyValue = dependencyGetter.invoke(bean);
if (!DependsOnUtils.isPropertyVisible(dependsOn, dependencyGetter.getReturnType(), dependencyPropertyValue))
return false;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new ExplicitException("Error invoking getter for property: " + dependsOn.property(), e);
}
}
ShowCondition showCondition = getter.getAnnotation(ShowCondition.class);
if (showCondition != null) {
EditContext.push(new EditContext() {
@Override
public Object getInputValue(String name) {
Method getter = BeanUtils.findGetter(bean.getClass(), name);
if (getter == null) {
throw new ExplicitException("Getter not found for property: " + name);
}
try {
return getter.invoke(bean);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new ExplicitException("Error invoking getter for property: " + dependsOn.property(), e);
}
}
});
try {
if (!(boolean)ReflectionUtils.invokeStaticMethod(bean.getClass(), showCondition.value()))
return false;
} finally {
EditContext.pop();
}
}
}
}
return true;
}
private boolean isCascadeRequired(BaseBeanValidationContext<?> validationContext, Object traversableObject, PathImpl path,
ConstraintLocationKind constraintLocationKind) {
if ( needToCallTraversableResolver( path, constraintLocationKind ) ) {
@ -1393,4 +1530,146 @@ public class ValidatorImpl implements Validator, ExecutableValidator {
private Object getCascadableValue(BaseBeanValidationContext<?> validationContext, Object object, Cascadable cascadable) {
return cascadable.getValue( object );
}
private void validatePropertyConstraintsForCurrentGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
if ( !valueContext.validatingDefault() ) {
validatePropertyConstraintsForNonDefaultGroup( validationContext, valueContext );
}
else {
validatePropertyConstraintsForDefaultGroup( validationContext, valueContext );
}
}
private void validateClassConstraintsForCurrentGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
if ( !valueContext.validatingDefault() ) {
validateClassConstraintsForNonDefaultGroup( validationContext, valueContext );
}
else {
validateClassConstraintsForDefaultGroup( validationContext, valueContext );
}
}
private void validatePropertyConstraintsForNonDefaultGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
Set<MetaConstraint<?>> propertyConstraints = filterPropertyConstraints(valueContext.getCurrentBeanMetaData().getMetaConstraints());
validateMetaConstraints( validationContext, valueContext, valueContext.getCurrentBean(), propertyConstraints );
validationContext.markCurrentBeanAsProcessed( valueContext );
}
private void validateClassConstraintsForNonDefaultGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
Set<MetaConstraint<?>> classConstraints = filterClassConstraints(valueContext.getCurrentBeanMetaData().getMetaConstraints());
validateMetaConstraints( validationContext, valueContext, valueContext.getCurrentBean(), classConstraints );
validationContext.markCurrentBeanAsProcessed( valueContext );
}
private <U> void validatePropertyConstraintsForDefaultGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<U, Object> valueContext) {
final BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
final Map<Class<?>, Class<?>> validatedInterfaces = new HashMap<>();
// evaluating the constraints of a bean per class in hierarchy, this is necessary to detect potential default group re-definitions
for ( Class<? super U> clazz : beanMetaData.getClassHierarchy() ) {
BeanMetaData<? super U> hostingBeanMetaData = beanMetaDataManager.getBeanMetaData( clazz );
boolean defaultGroupSequenceIsRedefined = hostingBeanMetaData.isDefaultGroupSequenceRedefined();
// if the current class redefined the default group sequence, this sequence has to be applied to all the class hierarchy.
if ( defaultGroupSequenceIsRedefined ) {
Iterator<Sequence> defaultGroupSequence = hostingBeanMetaData.getDefaultValidationSequence( valueContext.getCurrentBean() );
Set<MetaConstraint<?>> metaConstraints = filterPropertyConstraints(hostingBeanMetaData.getMetaConstraints());
while ( defaultGroupSequence.hasNext() ) {
for ( GroupWithInheritance groupOfGroups : defaultGroupSequence.next() ) {
boolean validationSuccessful = true;
for ( Group defaultSequenceMember : groupOfGroups ) {
validationSuccessful = validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz,
metaConstraints, defaultSequenceMember ) && validationSuccessful;
}
validationContext.markCurrentBeanAsProcessed( valueContext );
if ( !validationSuccessful ) {
break;
}
}
}
}
// fast path in case the default group sequence hasn't been redefined
else {
Set<MetaConstraint<?>> metaConstraints = filterPropertyConstraints(hostingBeanMetaData.getDirectMetaConstraints());
validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz, metaConstraints,
Group.DEFAULT_GROUP );
validationContext.markCurrentBeanAsProcessed( valueContext );
}
if ( defaultGroupSequenceIsRedefined ) {
break;
}
}
}
private <U> void validateClassConstraintsForDefaultGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<U, Object> valueContext) {
final BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
final Map<Class<?>, Class<?>> validatedInterfaces = new HashMap<>();
// evaluating the constraints of a bean per class in hierarchy, this is necessary to detect potential default group re-definitions
for ( Class<? super U> clazz : beanMetaData.getClassHierarchy() ) {
BeanMetaData<? super U> hostingBeanMetaData = beanMetaDataManager.getBeanMetaData( clazz );
boolean defaultGroupSequenceIsRedefined = hostingBeanMetaData.isDefaultGroupSequenceRedefined();
// if the current class redefined the default group sequence, this sequence has to be applied to all the class hierarchy.
if ( defaultGroupSequenceIsRedefined ) {
Iterator<Sequence> defaultGroupSequence = hostingBeanMetaData.getDefaultValidationSequence( valueContext.getCurrentBean() );
Set<MetaConstraint<?>> metaConstraints = filterClassConstraints(hostingBeanMetaData.getMetaConstraints());
while ( defaultGroupSequence.hasNext() ) {
for ( GroupWithInheritance groupOfGroups : defaultGroupSequence.next() ) {
boolean validationSuccessful = true;
for ( Group defaultSequenceMember : groupOfGroups ) {
validationSuccessful = validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz,
metaConstraints, defaultSequenceMember ) && validationSuccessful;
}
validationContext.markCurrentBeanAsProcessed( valueContext );
if ( !validationSuccessful ) {
break;
}
}
}
}
// fast path in case the default group sequence hasn't been redefined
else {
Set<MetaConstraint<?>> metaConstraints = filterClassConstraints(hostingBeanMetaData.getDirectMetaConstraints());
validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz, metaConstraints,
Group.DEFAULT_GROUP );
validationContext.markCurrentBeanAsProcessed( valueContext );
}
if ( defaultGroupSequenceIsRedefined ) {
break;
}
}
}
private Set<MetaConstraint<?>> filterPropertyConstraints(Set<MetaConstraint<?>> allConstraints) {
return allConstraints.stream()
.filter(this::isPropertyConstraint)
.collect(Collectors.toSet());
}
private Set<MetaConstraint<?>> filterClassConstraints(Set<MetaConstraint<?>> allConstraints) {
return allConstraints.stream()
.filter(this::isClassConstraint)
.collect(Collectors.toSet());
}
private boolean isPropertyConstraint(MetaConstraint<?> metaConstraint) {
ConstraintLocationKind kind = metaConstraint.getLocation().getKind();
return kind == ConstraintLocationKind.FIELD || kind == ConstraintLocationKind.GETTER;
}
private boolean isClassConstraint(MetaConstraint<?> metaConstraint) {
return metaConstraint.getLocation().getKind() == ConstraintLocationKind.TYPE;
}
}

@ -1 +1 @@
Subproject commit c4d70741345fe3851e7a8545fe7f6d92bd1f6137
Subproject commit 1d3daa461ecadfd75cd550740c583afdf2e76431

View File

@ -33,8 +33,7 @@ public class ServerShellModule extends AbstractPluginModule {
return Sets.newHashSet(ServerShellExecutor.class);
}
});
});
}
}