Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.codehaus.modello.model.ModelClass;
import org.codehaus.modello.model.ModelField;
import org.codehaus.modello.plugin.xsd.metadata.XsdClassMetadata;
import org.codehaus.modello.plugin.xsd.metadata.XsdFieldMetadata;
import org.codehaus.modello.plugins.xml.AbstractXmlGenerator;
import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
import org.codehaus.modello.plugins.xml.metadata.XmlFieldMetadata;
Expand Down Expand Up @@ -261,7 +262,10 @@ private void writeComplexTypeDescriptor(
} else if ((xsdType != null) || "char".equals(field.getType()) || "Character".equals(field.getType())) {
w.addAttribute("name", fieldTagName);

if (xsdType != null) {
XsdFieldMetadata xsdFieldMetadata = (XsdFieldMetadata) field.getMetadata(XsdFieldMetadata.ID);
boolean hasConstraint = hasXsdConstraint(xsdFieldMetadata);

if (xsdType != null && !hasConstraint) {
// schema built-in datatype
w.addAttribute("type", xsdType);
}
Expand All @@ -277,6 +281,8 @@ private void writeComplexTypeDescriptor(

if (xsdType == null) {
writeCharElement(w);
} else if (hasConstraint) {
writeConstrainedStringElement(w, xsdFieldMetadata);
}
} else {
// TODO cleanup/split this part it's no really human readable :-)
Expand Down Expand Up @@ -364,7 +370,10 @@ private void writeComplexTypeDescriptor(

w.addAttribute("name", tagName);

if (xsdType != null) {
XsdFieldMetadata xsdFieldMetadata = (XsdFieldMetadata) field.getMetadata(XsdFieldMetadata.ID);
boolean hasConstraint = hasXsdConstraint(xsdFieldMetadata);

if (xsdType != null && !hasConstraint) {
w.addAttribute("type", xsdType);
}

Expand All @@ -378,6 +387,8 @@ private void writeComplexTypeDescriptor(

if ("char".equals(field.getType()) || "Character".equals(field.getType())) {
writeCharElement(w);
} else if (hasConstraint) {
writeConstrainedStringElement(w, xsdFieldMetadata);
} else if (xsdType == null) {
throw new IllegalStateException("Attribute field of a non-primitive type '" + field.getType()
+ "' for '" + field.getName() + "' in '" + modelClass.getName() + "' model class");
Expand Down Expand Up @@ -488,6 +499,74 @@ private void writeListElement(
w.endElement();
}

private static boolean hasXsdConstraint(XsdFieldMetadata xsdFieldMetadata) {
return !xsdFieldMetadata.getEnumerations().isEmpty()
|| !xsdFieldMetadata.getSuggestions().isEmpty()
|| (xsdFieldMetadata.getPattern() != null
&& !xsdFieldMetadata.getPattern().isEmpty());
}

/**
* Writes an inline {@code xs:simpleType} for a constrained String field.
* <ul>
* <li>If {@link XsdFieldMetadata#getEnumerations()} is non-empty, generates a strict
* {@code xs:restriction} with one {@code xs:enumeration} per value.</li>
* <li>If {@link XsdFieldMetadata#getSuggestions()} is non-empty, generates an
* {@code xs:union} of a fixed enumeration and plain {@code xs:string}, giving
* IDE autocompletion without schema-level rejection of unknown values.</li>
* <li>If {@link XsdFieldMetadata#getPattern()} is set, generates a
* {@code xs:restriction base="xs:string"} with an {@code xs:pattern} facet.</li>
* </ul>
*/
private static void writeConstrainedStringElement(XMLWriter w, XsdFieldMetadata xsdFieldMetadata) {
if (!xsdFieldMetadata.getEnumerations().isEmpty()) {
// Strict enumeration: only the listed values are schema-valid.
w.startElement("xs:simpleType");
w.startElement("xs:restriction");
w.addAttribute("base", "xs:string");
for (String value : xsdFieldMetadata.getEnumerations()) {
w.startElement("xs:enumeration");
w.addAttribute("value", value);
w.endElement();
}
w.endElement(); // xs:restriction
w.endElement(); // xs:simpleType
} else if (!xsdFieldMetadata.getSuggestions().isEmpty()) {
// Non-strict union: IDE offers completion while still accepting any custom value.
w.startElement("xs:simpleType");
w.startElement("xs:union");
// memberTypes must reference named types — use an anonymous inner simpleType instead
w.startElement("xs:simpleType");
w.startElement("xs:restriction");
w.addAttribute("base", "xs:string");
for (String value : xsdFieldMetadata.getSuggestions()) {
w.startElement("xs:enumeration");
w.addAttribute("value", value);
w.endElement();
}
w.endElement(); // xs:restriction
w.endElement(); // xs:simpleType (enumeration member)
w.startElement("xs:simpleType");
w.startElement("xs:restriction");
w.addAttribute("base", "xs:string");
w.endElement(); // xs:restriction
w.endElement(); // xs:simpleType (xs:string member)
w.endElement(); // xs:union
w.endElement(); // xs:simpleType
} else if (xsdFieldMetadata.getPattern() != null
&& !xsdFieldMetadata.getPattern().isEmpty()) {
// Pattern restriction.
w.startElement("xs:simpleType");
w.startElement("xs:restriction");
w.addAttribute("base", "xs:string");
w.startElement("xs:pattern");
w.addAttribute("value", xsdFieldMetadata.getPattern());
w.endElement(); // xs:pattern
w.endElement(); // xs:restriction
w.endElement(); // xs:simpleType
}
}
Comment thread
gnodet marked this conversation as resolved.
Resolved

private static String getXsdType(String type) {
if ("String".equals(type)) {
return "xs:string";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
* limitations under the License.
*/

import java.util.Collections;
import java.util.List;

import org.codehaus.modello.metadata.FieldMetadata;

/**
Expand All @@ -25,4 +28,84 @@
*/
public class XsdFieldMetadata implements FieldMetadata {
public static final String ID = XsdFieldMetadata.class.getName();

/**
* Comma-separated allowed values for a strict {@code xs:restriction/xs:enumeration} constraint.
* When set, the generated XSD uses a {@code simpleType} with {@code restriction base="xs:string"}
* containing one {@code xs:enumeration} element per value. Instance documents that contain values
* outside this list are schema-invalid.
* <p>
* Mutually exclusive with {@link #suggestions} and {@link #pattern}.
* </p>
* <p>
* Modello syntax:
* <pre>{@code
* <field xsd.enumerations="compile,runtime,provided,test,system">
* <name>scope</name>
* <type>String</type>
* </field>
* }</pre>
*/
private List<String> enumerations = Collections.emptyList();

/**
* Comma-separated suggested values for a non-strict {@code xs:union} completion hint.
* When set, the generated XSD uses a {@code simpleType} with {@code union} of a fixed
* enumeration and {@code xs:string}, so IDEs offer autocompletion while still accepting
* any custom value.
* <p>
* Mutually exclusive with {@link #enumerations} and {@link #pattern}.
* </p>
* <p>
* Modello syntax:
* <pre>{@code
* <field xsd.suggestions="jar,pom,war,ear,maven-plugin">
* <name>packaging</name>
* <type>String</type>
* </field>
* }</pre>
*/
private List<String> suggestions = Collections.emptyList();

/**
* A regex pattern for a {@code xs:restriction/xs:pattern} constraint.
* When set, the generated XSD uses a {@code simpleType} with {@code restriction base="xs:string"}
* containing an {@code xs:pattern} element.
* <p>
* Mutually exclusive with {@link #enumerations} and {@link #suggestions}.
* </p>
* <p>
* Modello syntax:
* <pre>{@code
* <field xsd.pattern="[a-zA-Z0-9._\-]+">
* <name>groupId</name>
* <type>String</type>
* </field>
* }</pre>
*/
private String pattern;

public List<String> getEnumerations() {
return Collections.unmodifiableList(enumerations);
}
Comment thread
gnodet marked this conversation as resolved.
Resolved

public void setEnumerations(List<String> enumerations) {
this.enumerations = enumerations != null ? enumerations : Collections.emptyList();
}

public List<String> getSuggestions() {
return Collections.unmodifiableList(suggestions);
}

public void setSuggestions(List<String> suggestions) {
this.suggestions = suggestions != null ? suggestions : Collections.emptyList();
}

public String getPattern() {
return pattern;
}

public void setPattern(String pattern) {
this.pattern = pattern;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
import javax.inject.Named;
import javax.inject.Singleton;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.codehaus.modello.ModelloException;
import org.codehaus.modello.metadata.AbstractMetadataPlugin;
Expand Down Expand Up @@ -48,6 +52,12 @@ public class XsdMetadataPlugin extends AbstractMetadataPlugin {

public static final String COMPOSITOR = "xsd.compositor";

public static final String ENUMERATIONS = "xsd.enumerations";

public static final String SUGGESTIONS = "xsd.suggestions";

public static final String PATTERN = "xsd.pattern";

public AssociationMetadata getAssociationMetadata(ModelAssociation association, Map<String, String> data)
throws ModelloException {
return new XsdAssociationMetadata();
Expand All @@ -67,7 +77,27 @@ public InterfaceMetadata getInterfaceMetadata(ModelInterface iface, Map<String,
}

public FieldMetadata getFieldMetadata(ModelField field, Map<String, String> data) throws ModelloException {
return new XsdFieldMetadata();
XsdFieldMetadata metadata = new XsdFieldMetadata();

List<String> enumerations = parseCommaSeparated(getString(data, ENUMERATIONS));
List<String> suggestions = parseCommaSeparated(getString(data, SUGGESTIONS));
String pattern = getString(data, PATTERN);

long constraintCount = (enumerations.isEmpty() ? 0 : 1)
+ (suggestions.isEmpty() ? 0 : 1)
+ (pattern != null && !pattern.isEmpty() ? 1 : 0);
if (constraintCount > 1) {
throw new ModelloException("Field '"
+ field.getName()
+ "': xsd.enumerations, xsd.suggestions and xsd.pattern are mutually exclusive."
+ " Only one may be specified per field.");
}

metadata.setEnumerations(enumerations);
metadata.setSuggestions(suggestions);
metadata.setPattern(pattern);

return metadata;
}

public ModelMetadata getModelMetadata(Model model, Map<String, String> data) throws ModelloException {
Expand All @@ -79,4 +109,14 @@ public ModelMetadata getModelMetadata(Model model, Map<String, String> data) thr

return metadata;
}

private static List<String> parseCommaSeparated(String value) {
if (value == null || value.trim().isEmpty()) {
return Collections.emptyList();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}
Loading
Loading