001/*
002 * (C) Copyright 2006-2011 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 *     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)} 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<T>
076 * @deprecated since 10.3 use DefaultComponent descriptors management methods instead
077 */
078@Deprecated
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     * @param contrib
087     * @return
088     */
089    public abstract String getContributionId(T contrib);
090
091    /**
092     * Adds or updates a contribution.
093     * <p>
094     * If the contribution doesn't yet exists then it will be added, otherwise the value will be updated. If the given
095     * value is null the existing contribution must be removed.
096     * <p>
097     * The second parameter is the contribution that should be updated when merging, as well as stored and used. This
098     * usually represents a clone of the original contribution or a merge of multiple contribution fragments.
099     * Modifications on this object at application level will be lost on next
100     * {@link #contributionUpdated(String, Object, Object)} call on the same object id: modifications should be done in
101     * the {@link #merge(Object, Object)} method.
102     * <p>
103     * The last parameter is the new contribution object, unchanged (original) which was neither cloned nor merged. This
104     * object should never be modified at application level, because it will be used each time a subsequent merge is
105     * done. Also, it never should be stored.
106     *
107     * @param id - the id of the contribution that needs to be updated
108     * @param contrib the updated contribution object that
109     * @param newOrigContrib - the new, unchanged (original) contribution fragment that triggered the update.
110     */
111    public abstract void contributionUpdated(String id, T contrib, T newOrigContrib);
112
113    /**
114     * All the fragments in the contribution was removed. Contribution must be unregistered.
115     * <p>
116     * The first parameter is the contribution ID that should be remove and the second parameter the original
117     * contribution fragment that as unregistered causing the contribution to be removed.
118     *
119     * @param id
120     * @param origContrib
121     */
122    public abstract void contributionRemoved(String id, T origContrib);
123
124    /**
125     * CLone the given contribution object
126     *
127     * @param object
128     * @return
129     */
130    public abstract T clone(T orig);
131
132    /**
133     * Merge 'src' into 'dst'. When merging only the 'dst' object is modified.
134     *
135     * @param src the object to copy over the 'dst' object
136     * @param dst this object is modified
137     */
138    public abstract void merge(T src, T dst);
139
140    /**
141     * Returns true if merge is supported.
142     * <p>
143     * Hook method to be overridden if merge logics behind {@link #clone()} and {@link #merge(Object, Object)} cannot be
144     * implemented.
145     *
146     * @since 5.5
147     */
148    public boolean isSupportingMerge() {
149        return true;
150    }
151
152    /**
153     * Add a new contribution. This will start install the new contribution and will notify the implementation about the
154     * value to add. (the final value to add may not be the same object as the one added - but a merge between multiple
155     * contributions)
156     *
157     * @param contrib
158     */
159    public synchronized void addContribution(T contrib) {
160        String id = getContributionId(contrib);
161        FragmentList<T> head = addFragment(id, contrib);
162        contributionUpdated(id, head.merge(this), contrib);
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     * Uses standard equality to check for old objects (useEqualsMethod == false).
170     *
171     * @param contrib
172     * @see #removeContribution(Object, boolean)
173     */
174    public synchronized void removeContribution(T contrib) {
175        removeContribution(contrib, false);
176    }
177
178    /**
179     * Remove a contribution. This will uninstall the contribution and notify the implementation about the new value it
180     * should store (after re-merging contribution fragments).
181     * <p>
182     * Equality can be controlled from here.
183     * <p>
184     * Contributions come from the runtime that keeps exact instances, so using equality usually makes it possible to
185     * remove the exact instance that was contributed by this component (without needing to reference the component name
186     * for instance). But when unit-testing, or when registrating contributions that do not come directly from the
187     * runtime, regirties need to use the equals method defined on each contribution.
188     *
189     * @param contrib the contrib to remove
190     * @param useEqualsMethod a boolean stating that old contributions should be checked using the equals method instead
191     *            of
192     * @since 5.6
193     */
194    public synchronized void removeContribution(T contrib, boolean useEqualsMethod) {
195        String id = getContributionId(contrib);
196        FragmentList<T> head = removeFragment(id, contrib, useEqualsMethod);
197        if (head != null) {
198            T result = head.merge(this);
199            if (result != null) {
200                contributionUpdated(id, result, contrib);
201            } else {
202                contributionRemoved(id, contrib);
203            }
204        }
205    }
206
207    /**
208     * Get a merged contribution directly from the internal registry - and avoid passing by the implementation registry.
209     * Note that this operation will invoke a merge of existing fragments if needed.
210     * <p>
211     * Since 5.5, this method has made protected as it should not be used by the service retrieving merged resources
212     * (otherwise merge will be done again). If you'd really like to call it, add a public method on your registry
213     * implementation that will call it.
214     *
215     * @param id
216     * @return
217     */
218    protected synchronized T getContribution(String id) {
219        FragmentList<T> head = contribs.get(id);
220        return head != null ? head.merge(this) : null;
221    }
222
223    /**
224     * Get an array of all contribution fragments
225     *
226     * @return
227     */
228    @SuppressWarnings("unchecked")
229    public synchronized FragmentList<T>[] getFragments() {
230        return contribs.values().toArray(new FragmentList[contribs.size()]);
231    }
232
233    protected FragmentList<T> addFragment(String id, T contrib) {
234        FragmentList<T> head = contribs.get(id);
235        if (head == null) {
236            // no merge needed
237            head = new FragmentList<>();
238            this.contribs.put(id, head);
239        }
240        head.add(contrib);
241        return head;
242    }
243
244    protected FragmentList<T> removeFragment(String id, T contrib, boolean useEqualsMethod) {
245        FragmentList<T> head = contribs.get(id);
246        if (head == null) {
247            return null;
248        }
249        if (head.remove(contrib, useEqualsMethod)) {
250            if (head.isEmpty()) {
251                contribs.remove(id);
252            }
253            return head;
254        }
255        return null;
256    }
257
258    /**
259     * @since 9.3
260     */
261    public synchronized Map<String, T> toMap() {
262        return contribs.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> e.getValue().merge(this)));
263    }
264
265    public static class FragmentList<T> extends Fragment<T> {
266
267        public FragmentList() {
268            super(null);
269            prev = this;
270            next = this;
271        }
272
273        public boolean isEmpty() {
274            return next == null;
275        }
276
277        public T merge(ContributionFragmentRegistry<T> reg) {
278            T mergedValue = object;
279            if (mergedValue != null) {
280                return mergedValue;
281            }
282            Fragment<T> p = next;
283            if (p == this) {
284                return null;
285            }
286            mergedValue = reg.isSupportingMerge() ? reg.clone(p.object) : p.object;
287            p = p.next;
288            while (p != this) {
289                if (reg.isSupportingMerge()) {
290                    reg.merge(p.object, mergedValue);
291                } else {
292                    mergedValue = p.object;
293                }
294                p = p.next;
295            }
296            object = mergedValue;
297            return mergedValue;
298        }
299
300        public final void add(T contrib) {
301            insertBefore(new Fragment<>(contrib));
302            object = null;
303        }
304
305        public final void add(Fragment<T> fragment) {
306            insertBefore(fragment);
307            object = null;
308        }
309
310        public boolean remove(T contrib) {
311            return remove(contrib, false);
312        }
313
314        /**
315         * @since 5.6
316         */
317        public boolean remove(T contrib, boolean useEqualsMethod) {
318            Fragment<T> p = next;
319            while (p != this) {
320                if (useEqualsMethod && p.object != null && p.object.equals(contrib)
321                        || !useEqualsMethod && p.object == contrib) {
322                    p.remove();
323                    object = null;
324                    return true;
325                }
326                p = p.next;
327            }
328            return false;
329        }
330    }
331
332    public static class Fragment<T> {
333        public T object;
334
335        public Fragment<T> next;
336
337        public Fragment<T> prev;
338
339        public Fragment(T object) {
340            this.object = object;
341        }
342
343        public final void insertBefore(Fragment<T> fragment) {
344            fragment.prev = prev;
345            fragment.next = this;
346            prev.next = fragment;
347            prev = fragment;
348        }
349
350        public final void insertAfter(Fragment<T> fragment) {
351            fragment.prev = this;
352            fragment.next = next;
353            next.prev = fragment;
354            next = fragment;
355        }
356
357        public final void remove() {
358            prev.next = next;
359            next.prev = prev;
360            next = prev = null;
361        }
362
363        public final boolean hasNext() {
364            return next != null;
365        }
366
367        public final boolean hasPrev() {
368            return prev != null;
369        }
370    }
371
372}