001/*
002 * (C) Copyright 2006-2015 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 *     Florent Guillaume
018 */
019
020package org.nuxeo.ecm.core.storage.sql;
021
022import java.io.Serializable;
023import java.util.Arrays;
024import java.util.LinkedHashSet;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.commons.collections.map.ReferenceMap;
029import org.nuxeo.ecm.core.api.PropertyException;
030import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
031import org.nuxeo.ecm.core.storage.StateAccessor;
032
033/**
034 * A {@code Node} implementation. The actual data is stored in contained objects that are {@link Fragment}s.
035 */
036public class Node implements StateAccessor {
037
038    /** The persistence context used. */
039    private final PersistenceContext context;
040
041    private final Model model;
042
043    /** The hierarchy/main fragment. */
044    protected final SimpleFragment hierFragment;
045
046    /** Fragment information for each additional mixin or inherited fragment. */
047    protected final FragmentsMap fragments;
048
049    /**
050     * Path, only for immediate consumption after construction (will be reset to null afterwards).
051     */
052    protected String path;
053
054    /**
055     * Cache of property objects already retrieved. They are dumb objects, just providing an indirection to an
056     * underlying {@link Fragment}.
057     */
058    private final Map<String, BaseProperty> propertyCache;
059
060    private Boolean isVersion;
061
062    /**
063     * Creates a Node.
064     *
065     * @param context the persistence context
066     * @param fragmentGroup the group of fragments for the node
067     * @param path the path, if known at construction time
068     */
069    @SuppressWarnings("unchecked")
070    protected Node(PersistenceContext context, FragmentGroup fragmentGroup, String path) {
071        this.context = context;
072        model = context.model;
073        hierFragment = fragmentGroup.hier;
074        if (fragmentGroup.fragments == null) {
075            fragments = new FragmentsMap();
076        } else {
077            fragments = fragmentGroup.fragments;
078        }
079        this.path = path;
080        // memory-sensitive
081        propertyCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT);
082    }
083
084    // ----- basics -----
085
086    /**
087     * Gets the node unique id, usually a Long or a String.
088     *
089     * @return the node id
090     */
091    public Serializable getId() {
092        /*
093         * We don't cache the id as it changes between the initial creation and the first save.
094         */
095        return hierFragment.getId();
096    }
097
098    public String getName() {
099        return getHierFragment().getString(model.HIER_CHILD_NAME_KEY);
100    }
101
102    public Long getPos() {
103        return (Long) getHierFragment().get(model.HIER_CHILD_POS_KEY);
104    }
105
106    public String getPrimaryType() {
107        return hierFragment.getString(model.MAIN_PRIMARY_TYPE_KEY);
108    }
109
110    public Serializable getParentId() {
111        return getHierFragment().get(model.HIER_PARENT_KEY);
112    }
113
114    /**
115     * Gets the path that was assigned at {@link Node} construction time. Then it's reset to {@code null}. Should only
116     * be used once.
117     *
118     * @return the path, or {@code null} for unknown
119     */
120    public String getPath() {
121        String p = path;
122        if (p != null) {
123            path = null;
124        }
125        return p;
126    }
127
128    protected SimpleFragment getHierFragment() {
129        return hierFragment;
130    }
131
132    // cache the isVersion computation
133    public boolean isVersion() {
134        if (isVersion == null) {
135            isVersion = (Boolean) getSimpleProperty(model.MAIN_IS_VERSION_PROP).getValue();
136            if (isVersion == null) {
137                isVersion = Boolean.FALSE;
138            }
139        }
140        return isVersion.booleanValue();
141    }
142
143    public boolean isProxy() {
144        String primaryType = getPrimaryType();
145        if (primaryType == null) {
146            throw new NullPointerException(this.toString());
147        }
148        return primaryType.equals(model.PROXY_TYPE);
149    }
150
151    private static final String[] NO_MIXINS = {};
152
153    /**
154     * Gets the instance mixins. Mixins from the type are not returned.
155     * <p>
156     * Never returns {@code null}.
157     */
158    public String[] getMixinTypes() {
159        String[] value = (String[]) hierFragment.get(model.MAIN_MIXIN_TYPES_KEY);
160        return value == null ? NO_MIXINS : value.clone();
161    }
162
163    /**
164     * Gets the mixins. Includes mixins from the type. Returns a fresh set.
165     */
166    public Set<String> getAllMixinTypes() {
167        // linked for deterministic result
168        Set<String> mixins = new LinkedHashSet<String>(model.getDocumentTypeFacets(getPrimaryType()));
169        mixins.addAll(Arrays.asList(getMixinTypes()));
170        return mixins;
171    }
172
173    /**
174     * Checks the mixins. Includes mixins from the type.
175     */
176    public boolean hasMixinType(String mixin) {
177        if (model.getDocumentTypeFacets(getPrimaryType()).contains(mixin)) {
178            return true; // present in type
179        }
180        for (String m : getMixinTypes()) {
181            if (m.equals(mixin)) {
182                return true; // present in node
183            }
184        }
185        return false;
186    }
187
188    /**
189     * Clears the properties cache, used when removing mixins.
190     */
191    protected void clearCache() {
192        // some properties have now become invalid
193        propertyCache.clear();
194    }
195
196    // ----- properties -----
197
198    /**
199     * Gets a simple property from the node, given its name.
200     *
201     * @param name the property name
202     * @return the property
203     * @throws PropertyNotFoundException if the name is invalid
204     */
205    public SimpleProperty getSimpleProperty(String name) {
206        SimpleProperty property = (SimpleProperty) propertyCache.get(name);
207        if (property == null) {
208            ModelProperty propertyInfo = getPropertyInfo(name);
209            if (propertyInfo == null) {
210                throw new PropertyNotFoundException(name);
211            }
212            property = makeSimpleProperty(name, propertyInfo);
213            propertyCache.put(name, property);
214        }
215        return property;
216    }
217
218    protected SimpleProperty makeSimpleProperty(String name, ModelProperty propertyInfo) {
219        String fragmentName = propertyInfo.fragmentName;
220        Fragment fragment = fragments.get(fragmentName);
221        if (fragment == null) {
222            // lazy fragment, fetch from session
223            RowId rowId = new RowId(fragmentName, getId());
224            fragment = context.get(rowId, true);
225            fragments.put(fragmentName, fragment);
226        }
227        return new SimpleProperty(name, propertyInfo.propertyType, propertyInfo.readonly, (SimpleFragment) fragment,
228                propertyInfo.fragmentKey);
229    }
230
231    /**
232     * Gets a collection property from the node, given its name.
233     *
234     * @param name the property name
235     * @return the property
236     * @throws PropertyNotFoundException if the name is invalid
237     */
238    public CollectionProperty getCollectionProperty(String name) {
239        CollectionProperty property = (CollectionProperty) propertyCache.get(name);
240        if (property == null) {
241            ModelProperty propertyInfo = getPropertyInfo(name);
242            if (propertyInfo == null) {
243                throw new PropertyNotFoundException(name);
244            }
245            property = makeCollectionProperty(name, propertyInfo);
246            propertyCache.put(name, property);
247        }
248        return property;
249    }
250
251    protected CollectionProperty makeCollectionProperty(String name, ModelProperty propertyInfo) {
252        String fragmentName = propertyInfo.fragmentName;
253        Fragment fragment = fragments.get(fragmentName);
254        if (fragment == null) {
255            // lazy fragment, fetch from session
256            RowId rowId = new RowId(fragmentName, getId());
257            fragment = context.get(rowId, true);
258        }
259        if (fragment instanceof CollectionFragment) {
260            return new CollectionProperty(name, propertyInfo.propertyType, propertyInfo.readonly,
261                    (CollectionFragment) fragment);
262        } else {
263            fragments.put(fragmentName, fragment);
264            return new CollectionProperty(name, propertyInfo.propertyType, propertyInfo.readonly,
265                    (SimpleFragment) fragment, propertyInfo.fragmentKey);
266        }
267    }
268
269    protected ModelProperty getPropertyInfo(String name) {
270        // check primary type
271        ModelProperty propertyInfo = model.getPropertyInfo(getPrimaryType(), name);
272        if (propertyInfo != null) {
273            return propertyInfo;
274        }
275        // check mixins
276        for (String mixin : getMixinTypes()) {
277            propertyInfo = model.getMixinPropertyInfo(mixin, name);
278            if (propertyInfo != null) {
279                return propertyInfo;
280            }
281        }
282        // check proxy schemas
283        if (isProxy()) {
284            propertyInfo = model.getProxySchemasPropertyInfo(name);
285            if (propertyInfo != null) {
286                return propertyInfo;
287            }
288        }
289        return null;
290    }
291
292    public void setSimpleProperty(String name, Object value) {
293        SimpleProperty property = getSimpleProperty(name);
294        property.setValue(value);
295    }
296
297    public void setCollectionProperty(String name, Object[] value) {
298        CollectionProperty property = getCollectionProperty(name);
299        property.setValue(value);
300    }
301
302    // ----- locking -----
303
304    // ----- lifecycle -----
305
306    // ----- versioning -----
307
308    // ----- activities, baselines, configurations -----
309
310    // ----- shared nodes -----
311
312    // ----- retention -----
313
314    /*
315     * ----- equals/hashcode -----
316     */
317
318    @Override
319    public boolean equals(Object other) {
320        if (other == this) {
321            return true;
322        }
323        if (other instanceof Node) {
324            return equals((Node) other);
325        }
326        return false;
327    }
328
329    private boolean equals(Node other) {
330        return getId() == other.getId();
331    }
332
333    @Override
334    public int hashCode() {
335        return getId().hashCode();
336    }
337
338    @Override
339    public String toString() {
340        return getClass().getSimpleName() + "(uuid=" + getId() + ", name=" + getName() + ", primaryType="
341                + getPrimaryType() + ", parentId=" + getParentId() + ")";
342    }
343
344    @Override
345    public Object getSingle(String name) throws PropertyException {
346        return getSimpleProperty(name).getValue();
347    }
348
349    @Override
350    public Object[] getArray(String name) throws PropertyException {
351        return getCollectionProperty(name).getValue();
352    }
353
354    @Override
355    public void setSingle(String name, Object value) throws PropertyException {
356        getSimpleProperty(name).setValue(value);
357    }
358
359    @Override
360    public void setArray(String name, Object[] value) throws PropertyException {
361        getCollectionProperty(name).setValue(value);
362    }
363
364}