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    public boolean isRecord() {
152        return Boolean.TRUE.equals(getSimpleProperty(model.MAIN_IS_RECORD_PROP).getValue());
153    }
154
155    private static final String[] NO_MIXINS = {};
156
157    /**
158     * Gets the instance mixins. Mixins from the type are not returned.
159     * <p>
160     * Never returns {@code null}.
161     */
162    public String[] getMixinTypes() {
163        String[] value = (String[]) hierFragment.get(model.MAIN_MIXIN_TYPES_KEY);
164        return value == null ? NO_MIXINS : value.clone();
165    }
166
167    /**
168     * Gets the mixins. Includes mixins from the type. Returns a fresh set.
169     */
170    public Set<String> getAllMixinTypes() {
171        // linked for deterministic result
172        Set<String> mixins = new LinkedHashSet<>(model.getDocumentTypeFacets(getPrimaryType()));
173        mixins.addAll(Arrays.asList(getMixinTypes()));
174        return mixins;
175    }
176
177    /**
178     * Checks the mixins. Includes mixins from the type.
179     */
180    public boolean hasMixinType(String mixin) {
181        if (model.getDocumentTypeFacets(getPrimaryType()).contains(mixin)) {
182            return true; // present in type
183        }
184        for (String m : getMixinTypes()) {
185            if (m.equals(mixin)) {
186                return true; // present in node
187            }
188        }
189        return false;
190    }
191
192    /**
193     * Clears the properties cache, used when removing mixins.
194     */
195    protected void clearCache() {
196        // some properties have now become invalid
197        propertyCache.clear();
198    }
199
200    // ----- properties -----
201
202    /**
203     * Gets a simple property from the node, given its name.
204     *
205     * @param name the property name
206     * @return the property
207     * @throws PropertyNotFoundException if the name is invalid
208     */
209    public SimpleProperty getSimpleProperty(String name) {
210        SimpleProperty property = (SimpleProperty) propertyCache.get(name);
211        if (property == null) {
212            ModelProperty propertyInfo = getPropertyInfo(name);
213            if (propertyInfo == null) {
214                throw new PropertyNotFoundException(name);
215            }
216            property = makeSimpleProperty(name, propertyInfo);
217            propertyCache.put(name, property);
218        }
219        return property;
220    }
221
222    protected SimpleProperty makeSimpleProperty(String name, ModelProperty propertyInfo) {
223        String fragmentName = propertyInfo.fragmentName;
224        Fragment fragment = fragments.get(fragmentName);
225        if (fragment == null) {
226            // lazy fragment, fetch from session
227            RowId rowId = new RowId(fragmentName, getId());
228            fragment = context.get(rowId, true);
229            fragments.put(fragmentName, fragment);
230        }
231        return new SimpleProperty(name, propertyInfo.propertyType, propertyInfo.readonly, (SimpleFragment) fragment,
232                propertyInfo.fragmentKey);
233    }
234
235    /**
236     * Gets a collection property from the node, given its name.
237     *
238     * @param name the property name
239     * @return the property
240     * @throws PropertyNotFoundException if the name is invalid
241     */
242    public CollectionProperty getCollectionProperty(String name) {
243        CollectionProperty property = (CollectionProperty) propertyCache.get(name);
244        if (property == null) {
245            ModelProperty propertyInfo = getPropertyInfo(name);
246            if (propertyInfo == null) {
247                throw new PropertyNotFoundException(name);
248            }
249            property = makeCollectionProperty(name, propertyInfo);
250            propertyCache.put(name, property);
251        }
252        return property;
253    }
254
255    protected CollectionProperty makeCollectionProperty(String name, ModelProperty propertyInfo) {
256        String fragmentName = propertyInfo.fragmentName;
257        Fragment fragment = fragments.get(fragmentName);
258        if (fragment == null) {
259            // lazy fragment, fetch from session
260            RowId rowId = new RowId(fragmentName, getId());
261            fragment = context.get(rowId, true);
262        }
263        if (fragment instanceof CollectionFragment) {
264            return new CollectionProperty(name, propertyInfo.propertyType, propertyInfo.readonly,
265                    (CollectionFragment) fragment);
266        } else {
267            fragments.put(fragmentName, fragment);
268            return new CollectionProperty(name, propertyInfo.propertyType, propertyInfo.readonly,
269                    (SimpleFragment) fragment, propertyInfo.fragmentKey);
270        }
271    }
272
273    protected ModelProperty getPropertyInfo(String name) {
274        // check primary type
275        ModelProperty propertyInfo = model.getPropertyInfo(getPrimaryType(), name);
276        if (propertyInfo != null) {
277            return propertyInfo;
278        }
279        // check mixins
280        for (String mixin : getMixinTypes()) {
281            propertyInfo = model.getMixinPropertyInfo(mixin, name);
282            if (propertyInfo != null) {
283                return propertyInfo;
284            }
285        }
286        // check proxy schemas
287        if (isProxy()) {
288            propertyInfo = model.getProxySchemasPropertyInfo(name);
289            if (propertyInfo != null) {
290                return propertyInfo;
291            }
292        }
293        return null;
294    }
295
296    public void setSimpleProperty(String name, Object value) {
297        SimpleProperty property = getSimpleProperty(name);
298        property.setValue(value);
299    }
300
301    public void setCollectionProperty(String name, Object[] value) {
302        CollectionProperty property = getCollectionProperty(name);
303        property.setValue(value);
304    }
305
306    // ----- locking -----
307
308    // ----- lifecycle -----
309
310    // ----- versioning -----
311
312    // ----- activities, baselines, configurations -----
313
314    // ----- shared nodes -----
315
316    // ----- retention -----
317
318    /*
319     * ----- equals/hashcode -----
320     */
321
322    @Override
323    public boolean equals(Object other) {
324        if (other == this) {
325            return true;
326        }
327        if (other instanceof Node) {
328            return equals((Node) other);
329        }
330        return false;
331    }
332
333    private boolean equals(Node other) {
334        return getId() == other.getId();
335    }
336
337    @Override
338    public int hashCode() {
339        return getId().hashCode();
340    }
341
342    @Override
343    public String toString() {
344        return getClass().getSimpleName() + "(uuid=" + getId() + ", name=" + getName() + ", primaryType="
345                + getPrimaryType() + ", parentId=" + getParentId() + ")";
346    }
347
348    @Override
349    public Object getSingle(String name) throws PropertyException {
350        return getSimpleProperty(name).getValue();
351    }
352
353    @Override
354    public Object[] getArray(String name) throws PropertyException {
355        return getCollectionProperty(name).getValue();
356    }
357
358    @Override
359    public void setSingle(String name, Object value) throws PropertyException {
360        getSimpleProperty(name).setValue(value);
361    }
362
363    @Override
364    public void setArray(String name, Object[] value) throws PropertyException {
365        getCollectionProperty(name).setValue(value);
366    }
367
368}