001/*
002 * (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Bogdan Stefanescu
018 *     Wojciech Sulejman
019 *     Florent Guillaume
020 *     Thierry Delprat
021 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
022 */
023package org.nuxeo.ecm.core.schema;
024
025import static com.sun.xml.xsom.XSFacet.FACET_ENUMERATION;
026import static com.sun.xml.xsom.XSFacet.FACET_LENGTH;
027import static com.sun.xml.xsom.XSFacet.FACET_MAXEXCLUSIVE;
028import static com.sun.xml.xsom.XSFacet.FACET_MAXINCLUSIVE;
029import static com.sun.xml.xsom.XSFacet.FACET_MAXLENGTH;
030import static com.sun.xml.xsom.XSFacet.FACET_MINEXCLUSIVE;
031import static com.sun.xml.xsom.XSFacet.FACET_MININCLUSIVE;
032import static com.sun.xml.xsom.XSFacet.FACET_MINLENGTH;
033import static com.sun.xml.xsom.XSFacet.FACET_PATTERN;
034
035import java.io.File;
036import java.io.IOException;
037import java.net.URL;
038import java.util.ArrayList;
039import java.util.Collection;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.List;
043import java.util.Map;
044import java.util.Set;
045
046import org.apache.commons.logging.Log;
047import org.apache.commons.logging.LogFactory;
048import org.nuxeo.ecm.core.schema.types.ComplexType;
049import org.nuxeo.ecm.core.schema.types.ComplexTypeImpl;
050import org.nuxeo.ecm.core.schema.types.Field;
051import org.nuxeo.ecm.core.schema.types.ListType;
052import org.nuxeo.ecm.core.schema.types.ListTypeImpl;
053import org.nuxeo.ecm.core.schema.types.Schema;
054import org.nuxeo.ecm.core.schema.types.SchemaImpl;
055import org.nuxeo.ecm.core.schema.types.SimpleType;
056import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl;
057import org.nuxeo.ecm.core.schema.types.Type;
058import org.nuxeo.ecm.core.schema.types.TypeBindingException;
059import org.nuxeo.ecm.core.schema.types.TypeException;
060import org.nuxeo.ecm.core.schema.types.constraints.Constraint;
061import org.nuxeo.ecm.core.schema.types.constraints.ConstraintUtils;
062import org.nuxeo.ecm.core.schema.types.constraints.DateIntervalConstraint;
063import org.nuxeo.ecm.core.schema.types.constraints.EnumConstraint;
064import org.nuxeo.ecm.core.schema.types.constraints.LengthConstraint;
065import org.nuxeo.ecm.core.schema.types.constraints.NotNullConstraint;
066import org.nuxeo.ecm.core.schema.types.constraints.NumericIntervalConstraint;
067import org.nuxeo.ecm.core.schema.types.constraints.ObjectResolverConstraint;
068import org.nuxeo.ecm.core.schema.types.constraints.PatternConstraint;
069import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
070import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolverService;
071import org.nuxeo.runtime.api.Framework;
072import org.xml.sax.EntityResolver;
073import org.xml.sax.ErrorHandler;
074import org.xml.sax.InputSource;
075import org.xml.sax.SAXException;
076import org.xml.sax.SAXParseException;
077
078import com.sun.xml.xsom.ForeignAttributes;
079import com.sun.xml.xsom.XSAttributeDecl;
080import com.sun.xml.xsom.XSAttributeUse;
081import com.sun.xml.xsom.XSComplexType;
082import com.sun.xml.xsom.XSContentType;
083import com.sun.xml.xsom.XSElementDecl;
084import com.sun.xml.xsom.XSFacet;
085import com.sun.xml.xsom.XSListSimpleType;
086import com.sun.xml.xsom.XSModelGroup;
087import com.sun.xml.xsom.XSParticle;
088import com.sun.xml.xsom.XSSchema;
089import com.sun.xml.xsom.XSSchemaSet;
090import com.sun.xml.xsom.XSTerm;
091import com.sun.xml.xsom.XSType;
092import com.sun.xml.xsom.XmlString;
093import com.sun.xml.xsom.impl.RestrictionSimpleTypeImpl;
094import com.sun.xml.xsom.parser.XSOMParser;
095
096/**
097 * Loader of XSD schemas into Nuxeo Schema objects.
098 */
099public class XSDLoader {
100
101    private static final String ATTR_CORE_EXTERNAL_REFERENCES = "resolver";
102
103    private static final Log log = LogFactory.getLog(XSDLoader.class);
104
105    private static final String ANONYMOUS_TYPE_SUFFIX = "#anonymousType";
106
107    private static final String NAMESPACE_CORE_VALIDATION = "http://www.nuxeo.org/ecm/schemas/core/validation/";
108
109    private static final String NAMESPACE_CORE_EXTERNAL_REFERENCES = "http://www.nuxeo.org/ecm/schemas/core/external-references/";
110
111    private static final String NS_XSD = "http://www.w3.org/2001/XMLSchema";
112
113    protected final SchemaManagerImpl schemaManager;
114
115    protected List<String> referencedXSD = new ArrayList<>();
116
117    protected boolean collectReferencedXSD = false;
118
119    protected SchemaBindingDescriptor sd;
120
121    /**
122     * @deprecated since 11.1. Use {@link Framework#getService(Class)} with {@link ObjectResolverService} instead.
123     */
124    @Deprecated
125    protected ObjectResolverService getObjectResolverService() {
126        return Framework.getService(ObjectResolverService.class);
127    }
128
129    public XSDLoader(SchemaManagerImpl schemaManager) {
130        this.schemaManager = schemaManager;
131    }
132
133    public XSDLoader(SchemaManagerImpl schemaManager, SchemaBindingDescriptor sd) {
134        this.schemaManager = schemaManager;
135        this.sd = sd;
136    }
137
138    // used by Studio, do not remove
139    public XSDLoader(SchemaManagerImpl schemaManager, boolean collectReferencedXSD) {
140        this.schemaManager = schemaManager;
141        this.collectReferencedXSD = collectReferencedXSD;
142    }
143
144    protected void registerSchema(Schema schema) {
145        schemaManager.registerSchema(schema);
146    }
147
148    protected Type getType(String name) {
149        return schemaManager.getType(name);
150    }
151
152    protected XSOMParser getParser() {
153        XSOMParser parser = new XSOMParser();
154        ErrorHandler errorHandler = new SchemaErrorHandler();
155        parser.setErrorHandler(errorHandler);
156        if (sd != null) {
157            parser.setEntityResolver(new NXSchemaResolver(schemaManager, sd));
158        }
159        return parser;
160    }
161
162    protected static class NXSchemaResolver implements EntityResolver {
163
164        protected SchemaManagerImpl schemaManager;
165
166        protected SchemaBindingDescriptor sd;
167
168        NXSchemaResolver(SchemaManagerImpl schemaManager, SchemaBindingDescriptor sd) {
169            this.schemaManager = schemaManager;
170            this.sd = sd;
171        }
172
173        @Override
174        public InputSource resolveEntity(String publicId, String systemId) throws IOException {
175
176            String[] parts = systemId.split("/" + SchemaManagerImpl.SCHEMAS_DIR_NAME + "/");
177            String importXSDSubPath = parts[1];
178
179            File xsd = new File(schemaManager.getSchemasDir(), importXSDSubPath);
180            if (!xsd.exists()) {
181                int idx = sd.src.lastIndexOf("/");
182                importXSDSubPath = sd.src.substring(0, idx + 1) + importXSDSubPath;
183                URL url = sd.context.getLocalResource(importXSDSubPath);
184                if (url == null) {
185                    // try asking the class loader
186                    url = sd.context.getResource(importXSDSubPath);
187                }
188                if (url != null) {
189                    return new InputSource(url.openStream());
190                }
191            }
192
193            return null;
194        }
195
196    }
197
198    protected static class SchemaErrorHandler implements ErrorHandler {
199        @Override
200        public void error(SAXParseException e) throws SAXException {
201            log.error("Error: " + e.getMessage());
202            throw e;
203        }
204
205        @Override
206        public void fatalError(SAXParseException e) throws SAXException {
207            log.error("FatalError: " + e.getMessage());
208            throw e;
209        }
210
211        @Override
212        public void warning(SAXParseException e) {
213            log.error("Warning: " + e.getMessage());
214        }
215    }
216
217    // called by SchemaManagerImpl
218    public Schema loadSchema(String name, String prefix, File file) throws SAXException, IOException, TypeException {
219        return loadSchema(name, prefix, file, null);
220    }
221
222    /**
223     * Called by schema manager.
224     *
225     * @since 5.7
226     */
227    public Schema loadSchema(String name, String prefix, File file, String xsdElement)
228            throws SAXException, IOException, TypeException {
229        return loadSchema(name, prefix, file, xsdElement, false);
230    }
231
232    /**
233     * @param isVersionWritable if true, the schema's fields will be writable even for Version document.
234     * @since 8.4
235     */
236    public Schema loadSchema(String name, String prefix, File file, String xsdElement, boolean isVersionWritable)
237            throws SAXException, IOException, TypeException {
238        XSOMParser parser = getParser();
239        String systemId = file.toURI().toURL().toExternalForm();
240        if (file.getPath().startsWith("\\\\")) { // Windows UNC share
241            // work around a bug in Xerces due to
242            // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5086147
243            // (xsom passes a systemId of the form file://server/share/...
244            // but this is not parsed correctly when turned back into
245            // a File object inside Xerces)
246            systemId = systemId.replace("file://", "file:////");
247        }
248        try {
249            parser.parse(systemId);
250        } catch (SAXParseException e) {
251            throw new SAXException("Error parsing schema: " + systemId, e);
252        }
253
254        XSSchemaSet xsSchemas = parser.getResult();
255        if (collectReferencedXSD) {
256            collectReferencedXSD(xsSchemas);
257        }
258        return loadSchema(name, prefix, xsSchemas, xsdElement, isVersionWritable);
259    }
260
261    protected void collectReferencedXSD(XSSchemaSet xsSchemas) {
262
263        Collection<XSSchema> schemas = xsSchemas.getSchemas();
264        String ns;
265        for (XSSchema s : schemas) {
266            ns = s.getTargetNamespace();
267            if (ns.length() <= 0 || ns.equals(NS_XSD)) {
268                continue;
269            }
270
271            String systemId = s.getLocator().getSystemId();
272            if (systemId != null && systemId.startsWith("file:/")) {
273                String filePath = systemId.substring(6);
274                if (!referencedXSD.contains(filePath)) {
275                    referencedXSD.add(filePath);
276                }
277            }
278        }
279
280    }
281
282    /**
283     * Create Nuxeo schema from a XSD resource. If xsdElement is non null and correspont to the name of a complex
284     * element, the schema is created from the target complex type instead of from the global schema
285     *
286     * @since 5.7
287     * @param name schema name
288     * @param prefix schema prefix
289     * @param url url to load the XSD resource
290     * @param xsdElement name of the complex element to use as root of the schema
291     * @since 5.7
292     */
293    public Schema loadSchema(String name, String prefix, URL url, String xsdElement)
294            throws SAXException, TypeException {
295        XSOMParser parser = getParser();
296        parser.parse(url);
297        XSSchemaSet xsSchemas = parser.getResult();
298        return loadSchema(name, prefix, xsSchemas, xsdElement);
299    }
300
301    // called by tests
302    public Schema loadSchema(String name, String prefix, URL url) throws SAXException, TypeException {
303        return loadSchema(name, prefix, url, null);
304    }
305
306    /**
307     * @since 8.4
308     */
309    protected Schema loadSchema(String name, String prefix, XSSchemaSet schemaSet, String xsdElement)
310            throws TypeException {
311        return loadSchema(name, prefix, schemaSet, xsdElement, false);
312    }
313
314    protected Schema loadSchema(String name, String prefix, XSSchemaSet schemaSet, String xsdElement,
315            boolean isVersionWritable) throws TypeException {
316        if (schemaSet == null) {
317            return null;
318        }
319        Collection<XSSchema> schemas = schemaSet.getSchemas();
320        XSSchema schema = null;
321        String ns = null;
322        for (XSSchema s : schemas) {
323            ns = s.getTargetNamespace();
324            if (ns.length() > 0 && !ns.equals(NS_XSD)) {
325                schema = s;
326                break;
327            }
328        }
329        if (schema == null) {
330            return null;
331        }
332        Schema ecmSchema = new SchemaImpl(name, new Namespace(ns, prefix), isVersionWritable);
333
334        // load elements
335        Collection<XSElementDecl> elements = schema.getElementDecls().values();
336        for (XSElementDecl el : elements) {
337            // register the type if not yet registered
338            Type ecmType = loadType(ecmSchema, el.getType(), el.getName());
339            if (ecmType != null) {
340                // add the field to the schema
341                createField(ecmSchema, el, ecmType);
342            } else {
343                log.warn("Failed to load field " + el.getName() + " : " + el.getType());
344            }
345        }
346
347        // load attributes
348        Collection<XSAttributeDecl> attributes = schema.getAttributeDecls().values();
349        for (XSAttributeDecl att : attributes) {
350            // register the type if not yet registered
351            Type ecmType = loadType(ecmSchema, att.getType(), att.getName());
352            if (ecmType != null) {
353                // add the field to the schema
354                createField(ecmSchema, att, ecmType, true);
355            } else {
356                log.warn("Failed to load field from attribute " + att.getName() + " : " + att.getType());
357            }
358        }
359
360        if (xsdElement != null) {
361            Field singleComplexField = ecmSchema.getField(xsdElement);
362            if (singleComplexField == null) {
363                log.warn("Unable to find element " + xsdElement + " to rebase schema " + name);
364            } else {
365                if (singleComplexField.getType().isComplexType()) {
366                    ComplexType singleComplexFieldType = (ComplexType) singleComplexField.getType();
367                    ecmSchema = new SchemaImpl(singleComplexFieldType, name, new Namespace(ns, prefix),
368                            isVersionWritable);
369                } else {
370                    log.warn("can not rebase schema " + name + " on " + xsdElement + " that is not a complex type");
371                }
372            }
373        }
374
375        registerSchema(ecmSchema);
376        return ecmSchema;
377    }
378
379    /**
380     * @param schema the nuxeo schema into we register the type.
381     * @param type the XSD type to load
382     * @param fieldName the field name owning this type, this is used when type is anonymous/local
383     * @return the loaded type
384     */
385    protected Type loadType(Schema schema, XSType type, String fieldName) throws TypeBindingException {
386        String name = getTypeName(type, fieldName);
387        // look into global types
388        Type ecmType = getType(name);
389        if (ecmType != null) {
390            return ecmType;
391        }
392        // look into user types for this schema
393        ecmType = schema.getType(name);
394        if (ecmType != null) {
395            return ecmType;
396        }
397        // maybe an alias to a primitive type?
398        if (type.getTargetNamespace().equals(NS_XSD)) {
399            ecmType = XSDTypes.getType(name); // find alias
400            if (ecmType == null) {
401                log.warn("Cannot use unknown XSD type: " + name);
402            }
403            return ecmType;
404        }
405        if (type.isSimpleType()) {
406            if (type instanceof XSListSimpleType) {
407                ecmType = loadListType(schema, (XSListSimpleType) type, fieldName);
408            } else {
409                ecmType = loadSimpleType(schema, type, fieldName);
410            }
411        } else {
412            ecmType = loadComplexType(schema, name, type.asComplexType());
413        }
414        if (ecmType != null) {
415            schema.registerType(ecmType);
416        } else {
417            log.warn("loadType for " + fieldName + " of " + type + " returns null");
418        }
419        return ecmType;
420    }
421
422    /**
423     * @param name the type name (note, the type may have a null name if an anonymous type)
424     */
425    protected Type loadComplexType(Schema schema, String name, XSType type) throws TypeBindingException {
426        XSType baseType = type.getBaseType();
427        ComplexType superType = null;
428        // the anyType is the basetype of itself
429        if (baseType.getBaseType() != baseType) { // have a base type
430            if (baseType.isComplexType()) {
431                superType = (ComplexType) loadType(schema, baseType, name);
432            } else {
433                log.warn("Complex type has a non complex type super type???");
434            }
435        }
436        XSComplexType xsct = type.asComplexType();
437        // try to get the delta content
438        XSContentType content = xsct.getExplicitContent();
439        // if none get the entire content
440        if (content == null) {
441            content = xsct.getContentType();
442        }
443        Type ret = createComplexType(schema, superType, name, content, xsct.isAbstract());
444        if (ret != null && ret instanceof ComplexType) {
445            // load attributes if any
446            loadAttributes(schema, xsct, (ComplexType) ret);
447        }
448
449        return ret;
450    }
451
452    protected void loadAttributes(Schema schema, XSComplexType xsct, ComplexType ct) throws TypeBindingException {
453        Collection<? extends XSAttributeUse> attrs = xsct.getAttributeUses();
454        for (XSAttributeUse attr : attrs) {
455            XSAttributeDecl at = attr.getDecl();
456            Type fieldType = loadType(schema, at.getType(), at.getName());
457            if (fieldType == null) {
458                throw new TypeBindingException("Cannot add type for '" + at.getName() + "'");
459            }
460            createField(ct, at, fieldType, !attr.isRequired());
461        }
462    }
463
464    protected SimpleType loadSimpleType(Schema schema, XSType type, String fieldName) throws TypeBindingException {
465        String name = getTypeName(type, fieldName);
466        XSType baseType = type.getBaseType();
467        SimpleType superType = null;
468        if (baseType != type) {
469            // have a base type
470            superType = (SimpleType) loadType(schema, baseType, fieldName);
471        }
472        SimpleTypeImpl simpleType = new SimpleTypeImpl(superType, schema.getName(), name);
473
474        // add constraints/restrictions to the simple type
475        if (type instanceof RestrictionSimpleTypeImpl) {
476            RestrictionSimpleTypeImpl restrictionType = (RestrictionSimpleTypeImpl) type;
477
478            List<Constraint> constraints = new ArrayList<>();
479
480            // pattern
481            XSFacet patternFacet = restrictionType.getFacet(FACET_PATTERN);
482            if (patternFacet != null) {
483                if (simpleType.getPrimitiveType().support(PatternConstraint.class)) {
484                    // String pattern
485                    String pattern = patternFacet.getValue().toString();
486                    Constraint constraint = new PatternConstraint(pattern);
487                    constraints.add(constraint);
488                } else {
489                    logUnsupportedFacetRestriction(schema, fieldName, simpleType, FACET_PATTERN);
490                }
491            }
492
493            // length
494            XSFacet minLengthFacet = restrictionType.getFacet(FACET_MINLENGTH);
495            XSFacet maxLengthFacet = restrictionType.getFacet(FACET_MAXLENGTH);
496            XSFacet lengthFacet = restrictionType.getFacet(FACET_LENGTH);
497            if (maxLengthFacet != null || minLengthFacet != null || lengthFacet != null) {
498                if (simpleType.getPrimitiveType().support(LengthConstraint.class)) {
499                    // String Length
500                    Object min = null, max = null;
501                    if (lengthFacet != null) {
502                        min = lengthFacet.getValue().toString();
503                        max = min;
504                    } else {
505                        if (minLengthFacet != null) {
506                            min = minLengthFacet.getValue();
507                        }
508                        if (maxLengthFacet != null) {
509                            max = maxLengthFacet.getValue();
510                        }
511                    }
512                    Constraint constraint = new LengthConstraint(min, max);
513                    constraints.add(constraint);
514                } else {
515                    logUnsupportedFacetRestriction(schema, fieldName, simpleType, FACET_MINLENGTH, FACET_MAXLENGTH,
516                            FACET_LENGTH);
517                }
518            }
519
520            // Intervals
521            XSFacet minExclusiveFacet = restrictionType.getFacet(FACET_MINEXCLUSIVE);
522            XSFacet minInclusiveFacet = restrictionType.getFacet(FACET_MININCLUSIVE);
523            XSFacet maxExclusiveFacet = restrictionType.getFacet(FACET_MAXEXCLUSIVE);
524            XSFacet maxInclusiveFacet = restrictionType.getFacet(FACET_MAXINCLUSIVE);
525            if (minExclusiveFacet != null || minInclusiveFacet != null || maxExclusiveFacet != null
526                    || maxInclusiveFacet != null) {
527                if (simpleType.getPrimitiveType().support(NumericIntervalConstraint.class)) {
528                    // Numeric Interval
529                    Object min = null, max = null;
530                    boolean includingMin = true, includingMax = true;
531                    if (minExclusiveFacet != null) {
532                        min = minExclusiveFacet.getValue();
533                        includingMin = false;
534                    } else if (minInclusiveFacet != null) {
535                        min = minInclusiveFacet.getValue();
536                        includingMin = true;
537                    }
538                    if (maxExclusiveFacet != null) {
539                        max = maxExclusiveFacet.getValue();
540                        includingMax = false;
541                    } else if (maxInclusiveFacet != null) {
542                        max = maxInclusiveFacet.getValue();
543                        includingMax = true;
544                    }
545                    Constraint constraint = new NumericIntervalConstraint(min, includingMin, max, includingMax);
546                    constraints.add(constraint);
547                } else if (simpleType.getPrimitiveType().support(DateIntervalConstraint.class)) {
548                    // Date Interval
549                    Object min = null, max = null;
550                    boolean includingMin = true, includingMax = true;
551                    if (minExclusiveFacet != null) {
552                        min = minExclusiveFacet.getValue();
553                        includingMin = false;
554                    }
555                    if (minInclusiveFacet != null) {
556                        min = minInclusiveFacet.getValue();
557                        includingMin = true;
558                    }
559                    if (maxExclusiveFacet != null) {
560                        max = maxExclusiveFacet.getValue();
561                        includingMax = false;
562                    }
563                    if (maxInclusiveFacet != null) {
564                        max = maxInclusiveFacet.getValue();
565                        includingMax = true;
566                    }
567                    Constraint constraint = new DateIntervalConstraint(min, includingMin, max, includingMax);
568                    constraints.add(constraint);
569                } else {
570                    logUnsupportedFacetRestriction(schema, fieldName, simpleType, FACET_MINEXCLUSIVE,
571                            FACET_MININCLUSIVE, FACET_MAXEXCLUSIVE, FACET_MAXINCLUSIVE);
572                }
573            }
574
575            // Enumeration
576            List<XSFacet> enumFacets = restrictionType.getFacets("enumeration");
577            if (enumFacets != null && enumFacets.size() > 0) {
578                if (simpleType.getPrimitiveType().support(EnumConstraint.class)) {
579                    // string enumeration
580                    List<String> enumValues = new ArrayList<>();
581                    for (XSFacet enumFacet : enumFacets) {
582                        enumValues.add(enumFacet.getValue().toString());
583                    }
584                    Constraint constraint = new EnumConstraint(enumValues);
585                    constraints.add(constraint);
586                } else {
587                    logUnsupportedFacetRestriction(schema, fieldName, simpleType, FACET_ENUMERATION);
588                }
589            }
590
591            String refName = restrictionType.getForeignAttribute(NAMESPACE_CORE_EXTERNAL_REFERENCES,
592                    ATTR_CORE_EXTERNAL_REFERENCES);
593            Map<String, String> refParameters = new HashMap<>();
594            for (ForeignAttributes attr : restrictionType.getForeignAttributes()) {
595                for (int index = 0; index < attr.getLength(); index++) {
596                    String attrNS = attr.getURI(index);
597                    String attrName = attr.getLocalName(index);
598                    String attrValue = attr.getValue(index);
599                    if (NAMESPACE_CORE_EXTERNAL_REFERENCES.equals(attrNS)) {
600                        if (!ATTR_CORE_EXTERNAL_REFERENCES.equals(attrName)) {
601                            refParameters.put(attrName, attrValue);
602                        }
603                    }
604                }
605            }
606            if (refName != null) {
607                ObjectResolver resolver = Framework.getService(ObjectResolverService.class)
608                                                   .getResolver(refName, refParameters);
609                if (resolver != null) {
610                    simpleType.setResolver(resolver);
611                    constraints.add(new ObjectResolverConstraint(resolver));
612                } else {
613                    log.info("type of " + fieldName + "|" + type.getName()
614                            + " targets ObjectResolver namespace but has no matching resolver registered "
615                            + "(please contribute to component : org.nuxeo.ecm.core.schema.ObjectResolverService)");
616                }
617            }
618
619            simpleType.addConstraints(constraints);
620        }
621
622        return simpleType;
623    }
624
625    private void logUnsupportedFacetRestriction(Schema schema, String fieldName, SimpleTypeImpl simpleType,
626            String... facetNames) {
627        StringBuilder msg = new StringBuilder();
628        msg.append("schema|field|type : ").append(schema.getName());
629        msg.append("|").append(fieldName);
630        msg.append("|").append(simpleType.getPrimitiveType());
631        msg.append(" following restriction facet are not handled by constraints API for this type :");
632        for (String facetName : facetNames) {
633            msg.append(facetName).append(" ");
634        }
635        log.warn(msg.toString());
636    }
637
638    protected ListType loadListType(Schema schema, XSListSimpleType type, String fieldName)
639            throws TypeBindingException {
640        String name = getTypeName(type, fieldName);
641        XSType xsItemType = type.getItemType();
642        Type itemType;
643        if (xsItemType.getTargetNamespace().equals(NS_XSD)) {
644            itemType = XSDTypes.getType(xsItemType.getName());
645        } else {
646            itemType = loadSimpleType(schema, xsItemType, null);
647        }
648        if (itemType == null) {
649            log.error("list item type was not defined -> you should define first the item type");
650            return null;
651        }
652        return new ListTypeImpl(schema.getName(), name, itemType);
653    }
654
655    protected Type createComplexType(Schema schema, ComplexType superType, String name, XSContentType content,
656            boolean abstractType) throws TypeBindingException {
657
658        ComplexType ct = new ComplexTypeImpl(superType, schema.getName(), name);
659
660        // -------- Workaround - we register now the complex type - to fix
661        // recursive references to the same type
662        schema.registerType(ct);
663
664        // ------------------------------------------
665        XSParticle particle = content.asParticle();
666        if (particle == null) {
667            // complex type without particle -> may be it contains only
668            // attributes -> return it as is
669            return ct;
670        }
671        XSTerm term = particle.getTerm();
672        XSModelGroup mg = term.asModelGroup();
673
674        return processModelGroup(schema, superType, name, ct, mg, abstractType);
675    }
676
677    protected Type createFakeComplexType(Schema schema, ComplexType superType, String name, XSModelGroup mg)
678            throws TypeBindingException {
679
680        ComplexType ct = new ComplexTypeImpl(superType, schema.getName(), name);
681        // -------- Workaround - we register now the complex type - to fix
682        // recursive references to the same type
683        schema.registerType(ct);
684
685        return processModelGroup(schema, superType, name, ct, mg, false);
686    }
687
688    protected Type processModelGroup(Schema schema, ComplexType superType, String name, ComplexType ct, XSModelGroup mg,
689            boolean abstractType) throws TypeBindingException {
690        if (mg == null) {
691            // TODO don't know how to handle this for now
692            throw new TypeBindingException("unsupported complex type");
693        }
694        XSParticle[] group = mg.getChildren();
695        if (group.length == 0) {
696            return null;
697        }
698        if (group.length == 1 && superType == null && group[0].isRepeated()) {
699            // a list ?
700            // only convert to list of type is not abstract
701            if (!abstractType) {
702                return createListType(schema, name, group[0]);
703            }
704        }
705        for (XSParticle child : group) {
706            XSTerm term = child.getTerm();
707            XSElementDecl element = term.asElementDecl();
708            int maxOccur = child.getMaxOccurs().intValue();
709
710            if (element == null) {
711                // assume this is a xs:choice group
712                // (did not find any other way to detect !
713                //
714                // => make an aggregation of xs:choice subfields
715                if (maxOccur < 0 || maxOccur > 1) {
716                    // means this is a list
717                    //
718                    // first create a fake complex type
719                    Type fakeType = createFakeComplexType(schema, superType, name + "#anonymousListItem",
720                            term.asModelGroup());
721                    // wrap it as a list
722                    ListType listType = createListType(schema, name + "#anonymousListType", fakeType, 0, maxOccur);
723                    // add the listfield to the current CT
724                    String fieldName = ct.getName() + "#anonymousList";
725                    ct.addField(fieldName, listType, null, 0, null);
726                } else {
727                    processModelGroup(schema, superType, name, ct, term.asModelGroup(), abstractType);
728                }
729            } else {
730                XSType elementType = element.getType();
731                // type could be anonymous
732                // concat complex name to enforce inner element type unity across type
733                String fieldName = name + '#' + element.getName();
734                if (maxOccur < 0 || maxOccur > 1) {
735                    Type fieldType = loadType(schema, elementType, fieldName);
736                    if (fieldType != null) {
737                        ListType listType = createListType(schema, fieldName + "#anonymousListType", fieldType, 0,
738                                maxOccur);
739                        // add the listfield to the current CT
740                        ct.addField(element.getName(), listType, null, 0, null);
741                    }
742                } else {
743                    Type fieldType = loadType(schema, elementType, fieldName);
744                    if (fieldType != null) {
745                        createField(ct, element, fieldType);
746                    }
747                }
748            }
749        }
750
751        // add fields from Parent
752        if (superType != null && superType.isComplexType()) {
753            for (Field parentField : superType.getFields()) {
754                ct.addField(parentField.getName().getLocalName(), parentField.getType(),
755                        (String) parentField.getDefaultValue(), 0, null);
756            }
757        }
758        return ct;
759    }
760
761    protected ListType createListType(Schema schema, String name, XSParticle particle) throws TypeBindingException {
762        XSElementDecl element = particle.getTerm().asElementDecl();
763        if (element == null) {
764            log.warn("Ignoring " + name + " unsupported list type");
765            return null;
766        }
767        // type could be anonymous
768        // concat list name to enforce inner element type unity across type
769        Type type = loadType(schema, element.getType(), name + '#' + element.getName());
770        if (type == null) {
771            log.warn("Unable to find type for " + element.getName());
772            return null;
773        }
774
775        XmlString dv = element.getDefaultValue();
776        String defValue = null;
777        if (dv != null) {
778            defValue = dv.value;
779        }
780        int flags = 0;
781        if (defValue == null) {
782            dv = element.getFixedValue();
783            if (dv != null) {
784                defValue = dv.value;
785                flags |= Field.CONSTANT;
786            }
787        }
788        boolean computedNillable = isNillable(element);
789        if (computedNillable) {
790            flags |= Field.NILLABLE;
791        }
792
793        Set<Constraint> constraints = new HashSet<>();
794        if (!computedNillable) {
795            constraints.add(NotNullConstraint.get());
796        }
797        if (type instanceof SimpleType) {
798            SimpleType st = (SimpleType) type;
799            constraints.addAll(st.getConstraints());
800        }
801
802        return new ListTypeImpl(schema.getName(), name, type, element.getName(), defValue, flags, constraints,
803                particle.getMinOccurs().intValue(), particle.getMaxOccurs().intValue());
804    }
805
806    protected static ListType createListType(Schema schema, String name, Type itemType, int min, int max) {
807        String elementName = name + "#item";
808        return new ListTypeImpl(schema.getName(), name, itemType, elementName, null, min, max);
809    }
810
811    protected static Field createField(ComplexType type, XSElementDecl element, Type fieldType) {
812        String elementName = element.getName();
813        XmlString dv = element.getDefaultValue();
814        String defValue = null;
815        if (dv != null) {
816            defValue = dv.value;
817        }
818        int flags = 0;
819        if (defValue == null) {
820            dv = element.getFixedValue();
821            if (dv != null) {
822                defValue = dv.value;
823                flags |= Field.CONSTANT;
824            }
825        }
826
827        boolean computedNillable = isNillable(element);
828
829        if (computedNillable) {
830            flags |= Field.NILLABLE;
831        }
832
833        Set<Constraint> constraints = new HashSet<>();
834        if (!computedNillable) {
835            constraints.add(NotNullConstraint.get());
836        }
837        if (fieldType instanceof SimpleType) {
838            SimpleType st = (SimpleType) fieldType;
839            constraints.addAll(st.getConstraints());
840        }
841        Field field = type.addField(elementName, fieldType, defValue, flags, constraints);
842
843        // set the max field length from the constraints
844        if (fieldType instanceof SimpleTypeImpl) {
845            LengthConstraint lc = ConstraintUtils.getConstraint(field.getConstraints(), LengthConstraint.class);
846            if (lc != null && lc.getMax() != null) {
847                field.setMaxLength(lc.getMax().intValue());
848            }
849        }
850
851        return field;
852    }
853
854    protected static Field createField(ComplexType type, XSAttributeDecl element, Type fieldType, boolean isNillable) {
855        String elementName = element.getName();
856        XmlString dv = element.getDefaultValue();
857        String defValue = null;
858        if (dv != null) {
859            defValue = dv.value;
860        }
861        int flags = 0;
862        if (defValue == null) {
863            dv = element.getFixedValue();
864            if (dv != null) {
865                defValue = dv.value;
866                flags |= Field.CONSTANT;
867            }
868        }
869        Set<Constraint> constraints = new HashSet<>();
870        if (!isNillable) {
871            constraints.add(NotNullConstraint.get());
872        }
873        if (fieldType.isSimpleType()) {
874            constraints.addAll(fieldType.getConstraints());
875        }
876        return type.addField(elementName, fieldType, defValue, flags, constraints);
877    }
878
879    protected static String getTypeName(XSType type, String fieldName) {
880        String typeName = type.getName();
881        if (typeName == null || type.isLocal()) {
882            return getAnonymousTypeName(type, fieldName);
883        } else {
884            return typeName;
885        }
886    }
887
888    protected static String getAnonymousTypeName(XSType type, String fieldName) {
889        if (type.isComplexType()) {
890            XSElementDecl container = type.asComplexType().getScope();
891            String elName = container.getName();
892            return elName + ANONYMOUS_TYPE_SUFFIX;
893        } else {
894            return fieldName + ANONYMOUS_TYPE_SUFFIX;
895        }
896    }
897
898    // used by Studio, do not remove
899    public List<String> getReferencedXSD() {
900        return referencedXSD;
901    }
902
903    /**
904     * ignore case where xsd:nillable is recognized as false by xsom (we don't know if it's not specified and we want to
905     * preserve a default value to true. Therefore, we provide a custom attribute nxs:nillable to force nillable as
906     * false) NB: if xsd:nillable is present and sets to true, deducted value will be true even if nxs:nillable is false
907     *
908     * @since 7.1
909     */
910    protected static boolean isNillable(XSElementDecl element) {
911        String value = element.getForeignAttribute(NAMESPACE_CORE_VALIDATION, "nillable");
912        return element.isNillable() || value == null || Boolean.parseBoolean(value);
913    }
914
915}