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