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