001/*
002 * (C) Copyright 2006-2019 Nuxeo (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 *     bstefanescu
018 */
019package org.nuxeo.runtime.model;
020
021import java.util.HashMap;
022import java.util.Map;
023import java.util.Map.Entry;
024import java.util.stream.Collectors;
025
026/**
027 * This is a contribution registry that is managing contribution fragments and merge them as needed. The implementation
028 * will be notified through {@link #contributionUpdated(String, Object, Object)} each time you need to store or remove a
029 * contribution. Note that contribution objects that are registered by your implementation <b>must</b> not be modified.
030 * You can see them as immutable objects - otherwise your local changes will be lost at the next update event.
031 * <p>
032 * To use it you should extends this abstract implementation and implement the abstract methods.
033 * <p>
034 * The implementation registry doesn't need to be thread safe since it will be called from synchronized methods.
035 * <p>
036 * Also, the contribution object
037 * <p>
038 * A simple implementation is:
039 *
040 * <pre>
041 * public class MyRegistry extends ContributionFragmentRegistry&lt;MyContribution&gt; {
042 *     public Map&lt;String, MyContribution&gt; registry = new HAshMap&lt;String, MyContribution&gt;();
043 *
044 *     public String getContributionId(MyContribution contrib) {
045 *         return contrib.getId();
046 *     }
047 *
048 *     public void contributionUpdated(String id, MyContribution contrib, MyContribution origContrib) {
049 *         registry.put(id, contrib);
050 *     }
051 *
052 *     public void contributionRemoved(String id, MyContribution origContrib) {
053 *         registry.remove(id);
054 *     }
055 *
056 *     public MyContribution clone(MyContribution contrib) {
057 *          MyContribution clone = new MyContribution(contrib.getId());
058 *          clone.setSomeProperty(contrib.getSomeProperty());
059 *          ...
060 *          return clone;
061 *       }
062 *
063 *     public void merge(MyContribution src, MyContribution dst) {
064 *          dst.setSomeProperty(src.getSomeProperty());
065 *          ...
066 *       }
067 * }
068 * </pre>
069 *
070 * Since 5.5, if the registry does not support merging of resources, you can just override the method
071 * {@link #isSupportingMerge()} and return false, so that {@link #merge(Object, Object)} and {@link #clone()} are never
072 * called.
073 *
074 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
075 * @see SimpleContributionRegistry
076 * @deprecated since 10.3 use DefaultComponent descriptors management methods instead
077 */
078@Deprecated(since = "10.3")
079public abstract class ContributionFragmentRegistry<T> {
080
081    protected Map<String, FragmentList<T>> contribs = new HashMap<>();
082
083    /**
084     * Returns the contribution ID given the contribution object
085     */
086    public abstract String getContributionId(T contrib);
087
088    /**
089     * Adds or updates a contribution.
090     * <p>
091     * If the contribution doesn't yet exists then it will be added, otherwise the value will be updated. If the given
092     * value is null the existing contribution must be removed.
093     * <p>
094     * The second parameter is the contribution that should be updated when merging, as well as stored and used. This
095     * usually represents a clone of the original contribution or a merge of multiple contribution fragments.
096     * Modifications on this object at application level will be lost on next method call on the same object id:
097     * modifications should be done in the {@link #merge(Object, Object)} method.
098     * <p>
099     * The last parameter is the new contribution object, unchanged (original) which was neither cloned nor merged. This
100     * object should never be modified at application level, because it will be used each time a subsequent merge is
101     * done. Also, it never should be stored.
102     *
103     * @param id - the id of the contribution that needs to be updated
104     * @param contrib the updated contribution object that
105     * @param newOrigContrib - the new, unchanged (original) contribution fragment that triggered the update.
106     */
107    public abstract void contributionUpdated(String id, T contrib, T newOrigContrib);
108
109    /**
110     * All the fragments in the contribution was removed. Contribution must be unregistered.
111     * <p>
112     * The first parameter is the contribution ID that should be remove and the second parameter the original
113     * contribution fragment that as unregistered causing the contribution to be removed.
114     */
115    public abstract void contributionRemoved(String id, T origContrib);
116
117    /**
118     * CLone the given contribution object
119     */
120    public abstract T clone(T orig);
121
122    /**
123     * Merge 'src' into 'dst'. When merging only the 'dst' object is modified.
124     *
125     * @param src the object to copy over the 'dst' object
126     * @param dst this object is modified
127     */
128    public abstract void merge(T src, T dst);
129
130    /**
131     * Returns true if merge is supported.
132     * <p>
133     * Hook method to be overridden if merge logics behind {@link #clone()} and {@link #merge(Object, Object)} cannot be
134     * implemented.
135     *
136     * @since 5.5
137     */
138    public boolean isSupportingMerge() {
139        return true;
140    }
141
142    /**
143     * Add a new contribution. This will start install the new contribution and will notify the implementation about the
144     * value to add. (the final value to add may not be the same object as the one added - but a merge between multiple
145     * contributions)
146     */
147    public synchronized void addContribution(T contrib) {
148        String id = getContributionId(contrib);
149        FragmentList<T> head = addFragment(id, contrib);
150        contributionUpdated(id, head.merge(this), contrib);
151    }
152
153    /**
154     * Remove a contribution. This will uninstall the contribution and notify the implementation about the new value it
155     * should store (after re-merging contribution fragments).
156     * <p>
157     * Uses standard equality to check for old objects (useEqualsMethod == false).
158     *
159     * @see #removeContribution(Object, boolean)
160     */
161    public synchronized void removeContribution(T contrib) {
162        removeContribution(contrib, false);
163    }
164
165    /**
166     * Remove a contribution. This will uninstall the contribution and notify the implementation about the new value it
167     * should store (after re-merging contribution fragments).
168     * <p>
169     * Equality can be controlled from here.
170     * <p>
171     * Contributions come from the runtime that keeps exact instances, so using equality usually makes it possible to
172     * remove the exact instance that was contributed by this component (without needing to reference the component name
173     * for instance). But when unit-testing, or when registrating contributions that do not come directly from the
174     * runtime, regirties need to use the equals method defined on each contribution.
175     *
176     * @param contrib the contrib to remove
177     * @param useEqualsMethod a boolean stating that old contributions should be checked using the equals method instead
178     *            of
179     * @since 5.6
180     */
181    public synchronized void removeContribution(T contrib, boolean useEqualsMethod) {
182        String id = getContributionId(contrib);
183        FragmentList<T> head = removeFragment(id, contrib, useEqualsMethod);
184        if (head != null) {
185            T result = head.merge(this);
186            if (result != null) {
187                contributionUpdated(id, result, contrib);
188            } else {
189                contributionRemoved(id, contrib);
190            }
191        }
192    }
193
194    /**
195     * Get a merged contribution directly from the internal registry - and avoid passing by the implementation registry.
196     * Note that this operation will invoke a merge of existing fragments if needed.
197     * <p>
198     * Since 5.5, this method has made protected as it should not be used by the service retrieving merged resources
199     * (otherwise merge will be done again). If you'd really like to call it, add a public method on your registry
200     * implementation that will call it.
201     */
202    protected synchronized T getContribution(String id) {
203        FragmentList<T> head = contribs.get(id);
204        return head != null ? head.merge(this) : null;
205    }
206
207    /**
208     * Get an array of all contribution fragments
209     */
210    @SuppressWarnings("unchecked")
211    public synchronized FragmentList<T>[] getFragments() {
212        return contribs.values().toArray(new FragmentList[0]);
213    }
214
215    protected FragmentList<T> addFragment(String id, T contrib) {
216        FragmentList<T> head = contribs.get(id);
217        if (head == null) {
218            // no merge needed
219            head = new FragmentList<>();
220            this.contribs.put(id, head);
221        }
222        head.add(contrib);
223        return head;
224    }
225
226    protected FragmentList<T> removeFragment(String id, T contrib, boolean useEqualsMethod) {
227        FragmentList<T> head = contribs.get(id);
228        if (head == null) {
229            return null;
230        }
231        if (head.remove(contrib, useEqualsMethod)) {
232            if (head.isEmpty()) {
233                contribs.remove(id);
234            }
235            return head;
236        }
237        return null;
238    }
239
240    /**
241     * @since 9.3
242     */
243    public synchronized Map<String, T> toMap() {
244        return contribs.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> e.getValue().merge(this)));
245    }
246
247    public static class FragmentList<T> extends Fragment<T> {
248
249        public FragmentList() {
250            super(null);
251            prev = this;
252            next = this;
253        }
254
255        public boolean isEmpty() {
256            return next == null;
257        }
258
259        public T merge(ContributionFragmentRegistry<T> reg) {
260            T mergedValue = object;
261            if (mergedValue != null) {
262                return mergedValue;
263            }
264            Fragment<T> p = next;
265            if (p == this) {
266                return null;
267            }
268            mergedValue = reg.isSupportingMerge() ? reg.clone(p.object) : p.object;
269            p = p.next;
270            while (p != this) {
271                if (reg.isSupportingMerge()) {
272                    reg.merge(p.object, mergedValue);
273                } else {
274                    mergedValue = p.object;
275                }
276                p = p.next;
277            }
278            object = mergedValue;
279            return mergedValue;
280        }
281
282        public final void add(T contrib) {
283            insertBefore(new Fragment<>(contrib));
284            object = null;
285        }
286
287        public final void add(Fragment<T> fragment) {
288            insertBefore(fragment);
289            object = null;
290        }
291
292        public boolean remove(T contrib) {
293            return remove(contrib, false);
294        }
295
296        /**
297         * @since 5.6
298         */
299        public boolean remove(T contrib, boolean useEqualsMethod) {
300            Fragment<T> p = next;
301            while (p != this) {
302                if (useEqualsMethod && p.object != null && p.object.equals(contrib)
303                        || !useEqualsMethod && p.object == contrib) {
304                    p.remove();
305                    object = null;
306                    return true;
307                }
308                p = p.next;
309            }
310            return false;
311        }
312    }
313
314    public static class Fragment<T> {
315        public T object;
316
317        public Fragment<T> next;
318
319        public Fragment<T> prev;
320
321        public Fragment(T object) {
322            this.object = object;
323        }
324
325        public final void insertBefore(Fragment<T> fragment) {
326            fragment.prev = prev;
327            fragment.next = this;
328            prev.next = fragment;
329            prev = fragment;
330        }
331
332        public final void insertAfter(Fragment<T> fragment) {
333            fragment.prev = this;
334            fragment.next = next;
335            next.prev = fragment;
336            next = fragment;
337        }
338
339        public final void remove() {
340            prev.next = next;
341            next.prev = prev;
342            next = prev = null;
343        }
344
345        public final boolean hasNext() {
346            return next != null;
347        }
348
349        public final boolean hasPrev() {
350            return prev != null;
351        }
352    }
353
354}