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