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<MyContribution> { 042 * public Map<String, MyContribution> registry = new HAshMap<String, MyContribution>(); 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}