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