001/*
002 * Copyright (c) 2006-2012 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Bogdan Stefanescu
011 *     Florent Guillaume
012 */
013package org.nuxeo.ecm.core.schema.types;
014
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.HashMap;
018import java.util.Map;
019import java.util.Map.Entry;
020
021import org.nuxeo.ecm.core.schema.Namespace;
022import org.nuxeo.ecm.core.schema.TypeConstants;
023import org.nuxeo.ecm.core.schema.types.constraints.Constraint;
024
025/**
026 * A Complex Type holds several fields.
027 */
028public class ComplexTypeImpl extends AbstractType implements ComplexType {
029
030    private static final long serialVersionUID = 1L;
031
032    /** The fields held by this complex type. */
033    protected final Map<QName, Field> fields = new HashMap<QName, Field>();
034
035    /** The map of name or prefixed name to field. */
036    protected volatile Map<String, Field> fieldsByName = new HashMap<String, Field>();
037
038    protected final Namespace ns;
039
040    public ComplexTypeImpl(ComplexType superType, String schema, String name, Namespace ns) {
041        super(superType, schema, name);
042        // for composite types, they already include schemas from supertypes
043        // if (superType != null) {
044        // for (Field field : superType.getFields()) {
045        // addField(field);
046        // }
047        // }
048        this.ns = ns;
049    }
050
051    public ComplexTypeImpl(ComplexType superType, String schema, String name) {
052        this(superType, schema, name, Namespace.DEFAULT_NS);
053    }
054
055    // also called by CompositeTypeImpl
056    protected void addField(Field field) {
057        QName name = field.getName();
058        fields.put(name, field);
059        fieldsByName.put(name.getLocalName(), field);
060        fieldsByName.put(name.getPrefixedName(), field);
061    }
062
063    // called by XSDLoader
064    @Override
065    public Field addField(String name, Type type, String defaultValue, int flags, Collection<Constraint> constraints) {
066        QName qname = QName.valueOf(name, ns.prefix);
067        FieldImpl field = new FieldImpl(qname, this, type, defaultValue, flags, constraints);
068        addField(field);
069        return field;
070    }
071
072    @Override
073    public Namespace getNamespace() {
074        return ns;
075    }
076
077    @Override
078    public Field getField(String name) {
079        return fieldsByName.get(name);
080    }
081
082    @Override
083    public Field getField(QName name) {
084        return fields.get(name);
085    }
086
087    @Override
088    public Collection<Field> getFields() {
089        return fields.values();
090    }
091
092    @Override
093    public int getFieldsCount() {
094        return fields.size();
095    }
096
097    @Override
098    public boolean hasField(String name) {
099        return fieldsByName.containsKey(name);
100    }
101
102    @Override
103    public boolean hasFields() {
104        return !fields.isEmpty();
105    }
106
107    @Override
108    public boolean isComplexType() {
109        return true;
110    }
111
112    @SuppressWarnings("unchecked")
113    @Override
114    public boolean validate(Object object) throws TypeException {
115        if (object == null) {
116            return true;
117        }
118        if (object instanceof Map) {
119            return validateMap((Map<Object, Object>) object);
120        }
121        return false;
122    }
123
124    protected boolean validateMap(Map<Object, Object> map) {
125        return true;
126    }
127
128    @Override
129    public String toString() {
130        return getClass().getSimpleName() + '(' + name + ')';
131    }
132
133    @Override
134    public Map<String, Object> newInstance() {
135        if (TypeConstants.isContentType(this)) {
136            // NXP-912: should return null for a blob. Since there is no
137            // pluggable adapter mechanism on types, and since document model
138            // properties consider that every complex property named "content"
139            // should be dealt with a BlobProperty, this is hardcoded here.
140            return null;
141        }
142        Map<String, Object> map = new HashMap<String, Object>();
143        for (Field field : fields.values()) {
144            Object value;
145            Type type = field.getType();
146            if (type.isComplexType()) {
147                value = type.newInstance();
148            } else if (type.isListType()) {
149                value = new ArrayList<Object>();
150            } else {
151                value = field.getDefaultValue();
152            }
153            map.put(field.getName().getLocalName(), value);
154        }
155        return map;
156    }
157
158    @Override
159    @SuppressWarnings("unchecked")
160    public Object convert(Object object) throws TypeException {
161        if (object instanceof Map) {
162            Map<Object, Object> map = (Map<Object, Object>) object;
163            for (Entry<Object, Object> entry : map.entrySet()) {
164                String key = entry.getKey().toString();
165                Field field = getField(key);
166                if (field == null) {
167                    throw new IllegalArgumentException("Field " + key + " is not defined for the complex type "
168                            + getName());
169                }
170                entry.setValue(field.getType().convert(entry.getValue()));
171            }
172            return object;
173        }
174        throw new TypeException("Incompatible object: " + object.getClass() + " for type " + this);
175    }
176
177    /**
178     * Canonicalizes a Nuxeo-xpath.
179     * <p>
180     * Replaces {@code a/foo[123]/b} with {@code a/123/b}
181     * <p>
182     * A star can be used instead of the digits as well (for configuration).
183     *
184     * @param xpath the xpath
185     * @return the canonicalized xpath.
186     */
187    public static String canonicalXPath(String xpath) {
188        while (xpath.length() > 0 && xpath.charAt(0) == '/') {
189            xpath = xpath.substring(1);
190        }
191        if (xpath.indexOf('[') == -1) {
192            return xpath;
193        } else {
194            return xpath.replaceAll("[^/\\[\\]]+\\[(\\d+|\\*)\\]", "$1");
195        }
196    }
197
198}