Merge branch 'release/5.45.0'
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / agent / Team.java
1 /**
2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.model.agent;
10
11 import java.beans.PropertyChangeEvent;
12 import java.beans.PropertyChangeListener;
13 import java.util.ArrayList;
14 import java.util.List;
15
16 import javax.persistence.Entity;
17 import javax.persistence.FetchType;
18 import javax.persistence.ManyToMany;
19 import javax.persistence.OrderColumn;
20 import javax.persistence.Transient;
21 import javax.xml.bind.annotation.XmlAccessType;
22 import javax.xml.bind.annotation.XmlAccessorType;
23 import javax.xml.bind.annotation.XmlElement;
24 import javax.xml.bind.annotation.XmlElementWrapper;
25 import javax.xml.bind.annotation.XmlIDREF;
26 import javax.xml.bind.annotation.XmlRootElement;
27 import javax.xml.bind.annotation.XmlSchemaType;
28 import javax.xml.bind.annotation.XmlType;
29
30 import org.apache.logging.log4j.LogManager;
31 import org.apache.logging.log4j.Logger;
32 import org.hibernate.annotations.Cascade;
33 import org.hibernate.annotations.CascadeType;
34 import org.hibernate.annotations.ListIndexBase;
35 import org.hibernate.envers.Audited;
36 import org.springframework.beans.factory.annotation.Configurable;
37
38 import eu.etaxonomy.cdm.common.CdmUtils;
39 import eu.etaxonomy.cdm.model.EntityCollectionSetterAdapter;
40 import eu.etaxonomy.cdm.model.EntityCollectionSetterAdapter.SetterAdapterException;
41 import eu.etaxonomy.cdm.strategy.cache.agent.TeamDefaultCacheStrategy;
42 import eu.etaxonomy.cdm.strategy.match.Match;
43 import eu.etaxonomy.cdm.strategy.match.MatchMode;
44
45 /**
46 * This class represents teams of {@link Person persons}. A team exists either for itself
47 * or is built with the list of (distinct) persons who belong to it.
48 * In the first case the inherited attribute {@link eu.etaxonomy.cdm.model.common.IdentifiableEntity#getTitleCache() titleCache} is to be used.
49 * In the second case at least all abbreviated names
50 * (the inherited attributes {@link TeamOrPersonBase#getNomenclaturalTitle() nomenclaturalTitle})
51 * or all full names (the strings returned by Person.generateTitle)
52 * of the persons must exist. A team is a {@link java.util.List list} of persons.
53 * <P>
54 * This class corresponds to: <ul>
55 * <li> Team according to the TDWG ontology
56 * <li> AgentNames (partially) according to the TCS
57 * <li> MicroAgent (partially) according to the ABCD schema
58 * </ul>
59 *
60 * @author m.doering
61 * @since 08-Nov-2007 13:06:58
62 */
63 @XmlAccessorType(XmlAccessType.FIELD)
64 @XmlType(name = "Team", propOrder = {
65 "protectedNomenclaturalTitleCache",
66 "protectedCollectorTitleCache",
67 "teamMembers",
68 "hasMoreMembers"
69 })
70 @XmlRootElement(name = "Team")
71 @Entity
72 @Audited
73 @Configurable
74 public class Team extends TeamOrPersonBase<Team> {
75
76 private static final long serialVersionUID = 97640416905934622L;
77 private static final Logger logger = LogManager.getLogger();
78
79 @XmlElement(name = "ProtectedNomenclaturalTitleCache")
80 private boolean protectedNomenclaturalTitleCache = false;
81
82 @XmlElement(name = "ProtectedCollectorTitleCache")
83 private boolean protectedCollectorTitleCache = false;
84
85 //An abbreviated name for the team (e. g. in case of nomenclatural authorteams).
86 //A non abbreviated name for the team (e. g.
87 //in case of some bibliographical references)
88 @XmlElementWrapper(name = "TeamMembers", nillable = true)
89 @XmlElement(name = "TeamMember")
90 @XmlIDREF
91 @XmlSchemaType(name = "IDREF")
92 @OrderColumn(name="sortIndex")
93 @ListIndexBase(value=0) //not really needed as this is the default
94 @ManyToMany(fetch = FetchType.LAZY)
95 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
96 @Match(MatchMode.MATCH)
97 private List<Person> teamMembers;
98
99 @XmlElement(name = "hasMoreMembers")
100 private boolean hasMoreMembers;
101
102 // ********************************** FACTORY ***************************/
103
104 /**
105 * Creates a new team instance without any concrete {@link Person members}.
106 */
107 static public Team NewInstance(){
108 return new Team();
109 }
110
111 /**
112 * Creates a new team instance with a bibliographic and nomenclatural title
113 * but without any {@link Person members}. The caches are set to protected.
114 */
115 static public Team NewTitledInstance(String title, String nomTitle){
116 Team result = new Team();
117 if (isNotBlank(title)){
118 result.setTitleCache(title, true);
119 }
120 if (isNotBlank(nomTitle)){
121 result.setNomenclaturalTitleCache(nomTitle, true);
122 }
123 return result;
124 }
125
126 /**
127 * Creates a new team instance with a bibliographic, nomenclatural and collector title
128 * but without any {@link Person members}. The caches are set to protected if not blank.
129 */
130 static public Team NewTitledInstance(String title, String nomTitle, String collectorTitle){
131 Team result = new Team();
132 if (isNotBlank(title)){
133 result.setTitleCache(title, true);
134 }
135 if (isNotBlank(nomTitle)){
136 result.setNomenclaturalTitleCache(nomTitle, true);
137 }
138 if (isNotBlank(collectorTitle)){
139 result.setCollectorTitleCache(collectorTitle, true);
140 }
141 return result;
142 }
143
144 public static Team NewInstance(Person... members) {
145 Team team = new Team();
146 for (Person member : members){
147 team.addTeamMember(member);
148 }
149 return team;
150 }
151
152 //************************ CONSTRUCTOR *******************************/
153
154 /**
155 * Class constructor (including the cache strategy defined in
156 * {@link eu.etaxonomy.cdm.strategy.cache.agent.TeamDefaultCacheStrategy TeamDefaultCacheStrategy}).
157 */
158 public Team() {
159 addListenersToMembers();
160 }
161
162 // ******************************************************************/
163
164 @Override
165 protected void initDefaultCacheStrategy() {
166 this.cacheStrategy = TeamDefaultCacheStrategy.NewInstance();
167 }
168
169 @Override
170 public void initListener(){
171 PropertyChangeListener listener = new PropertyChangeListener() {
172 @Override
173 public void propertyChange(PropertyChangeEvent ev) {
174 if (!ev.getPropertyName().equals("titleCache") &&
175 !ev.getPropertyName().equals("collectorTitleCache") &&
176 !ev.getPropertyName().equals("nomenclaturalTitleCache") &&
177 !ev.getPropertyName().equals("cacheStrategy")){
178 resetCaches();
179 }
180 }
181 };
182 addPropertyChangeListener(listener);
183 }
184
185 /**
186 * Adds a property change listener to all team members.
187 */
188 private void addListenersToMembers() {
189 List<Person> members = getTeamMembers();
190 for (Person member : members){
191 addListenerForTeamMember(member);
192 }
193 }
194
195 private void addListenerForTeamMember(Person member) {
196 PropertyChangeListener listener = new PropertyChangeListener() {
197 @Override
198 public void propertyChange(PropertyChangeEvent e) {
199 resetCaches();
200 }
201 };
202 member.addPropertyChangeListener(listener);
203 }
204
205 public void resetCaches() {
206 if(!protectedTitleCache){
207 titleCache = null;
208 }
209 if (! protectedNomenclaturalTitleCache){
210 nomenclaturalTitleCache = null;
211 }
212 if (!protectedCollectorTitleCache){
213 collectorTitleCache = null;
214 }
215 }
216
217 /**
218 * Returns the list of {@link Person members} belonging to <i>this</i> team.
219 * A person may be a member of several distinct teams.
220 */
221 public List<Person> getTeamMembers(){
222 if(teamMembers == null) {
223 this.teamMembers = new ArrayList<>();
224 }
225 return this.teamMembers;
226 }
227
228 public void setTeamMembers(List<Person> teamMembers) throws SetterAdapterException {
229 new EntityCollectionSetterAdapter<Team, Person>(Team.class, Person.class, "teamMembers").setCollection(this, teamMembers);
230 }
231
232 /**
233 * Adds a new {@link Person person} to <i>this</i> team at the end of the members' list.
234 *
235 * @param person the person who should be added to the other team members
236 * @see #getTeamMembers()
237 * @see Person
238 */
239 public void addTeamMember(Person person){
240 if (person != null){
241 getTeamMembers().add(person);
242 firePropertyChange("teamMember", null, person);
243 addListenerForTeamMember(person);
244 }
245 }
246
247 /**
248 * Adds a new {@link Person person} to <i>this</i> team
249 * at the given index place of the members' list. If the person is already
250 * a member of the list he will be moved to the given index place.
251 * The index must be an integer (>=0). If the index is larger than
252 * the present number of members the person will be added at the end of the list.
253 *
254 * @param person the person who should be added to the other team members
255 * @param index the position at which the person should be placed within the members' list (starting with 0)
256 * @see #getTeamMembers()
257 * @see Person
258 */
259 public void addTeamMember(Person person, int index){
260 if (person != null){
261 int oldIndex = getTeamMembers().indexOf(person);
262 if (oldIndex != -1 ){
263 getTeamMembers().remove(person);
264 }
265 if (index >= getTeamMembers().size()){
266 index = getTeamMembers().size();
267 }
268 getTeamMembers().add(index, person);
269 addListenerForTeamMember(person);
270 firePropertyChange("teamMember", null, person);
271 }
272 }
273
274 /**
275 * Removes one person from the list of members of <i>this</i> team.
276 *
277 * @param person the person who should be deleted from <i>this</i> team
278 * @see #getTeamMembers()
279 */
280 public void removeTeamMember(Person person){
281 boolean wasMember = getTeamMembers().remove(person);
282 if (wasMember){
283 firePropertyChange("teamMember", person, null);
284 }
285 }
286
287 public boolean replaceTeamMember(Person newObject, Person oldObject){
288 return replaceInList(this.teamMembers, newObject, oldObject);
289 }
290
291 /**
292 * Generates or returns the {@link TeamOrPersonBase#getnomenclaturalTitle() nomenclatural identification} string for <i>this</i> team.
293 * This method overrides {@link TeamOrPersonBase#getNomenclaturalTitle() getNomenclaturalTitle}.
294 * This string is built with the {@link TeamOrPersonBase#getNomenclaturalTitle() abbreviated names}
295 * of all persons belonging to its (ordered) members' list if the flag
296 * {@link #protectedNomenclaturalTitleCache protectedNomenclaturalTitleCache} is not set.
297 * Otherwise this method returns the present nomenclatural abbreviation.
298 * In case the string is generated the cache strategy used is defined in
299 * {@link eu.etaxonomy.cdm.strategy.cache.agent.TeamDefaultCacheStrategy TeamDefaultCacheStrategy}.
300 * The result might be kept as nomenclatural abbreviation
301 * by using the {@link #setNomenclaturalTitle(String) setNomenclaturalTitle} method.
302 *
303 * @return a string which identifies <i>this</i> team for nomenclature
304 */
305 @Override
306 @Transient
307 public String getNomenclaturalTitleCache() {
308 if (protectedNomenclaturalTitleCache == PROTECTED){
309 return this.nomenclaturalTitleCache;
310 }
311 if (nomenclaturalTitleCache == null){
312 this.nomenclaturalTitleCache = getTruncatedCache(cacheStrategy().getNomenclaturalTitleCache(this));
313 }else{
314 //as long as team members do not inform the team about changes the cache must be created new each time
315 nomenclaturalTitleCache = getTruncatedCache(cacheStrategy().getNomenclaturalTitleCache(this));
316 }
317 return nomenclaturalTitleCache;
318 }
319
320 /**
321 * Assigns a {@link TeamOrPersonBase#nomenclaturalTitle nomenclatural identification} string to <i>this</i> team
322 * and a protection flag status to this string.
323 *
324 * @see #getNomenclaturalTitleCache()
325 */
326 @Override
327 public void setNomenclaturalTitleCache(String nomenclaturalTitleCache, boolean protectedNomenclaturalTitleCache) {
328 firePropertyChange("nomenclaturalTitleCache", this.nomenclaturalTitleCache, nomenclaturalTitleCache);
329 this.nomenclaturalTitleCache = CdmUtils.Nb(nomenclaturalTitleCache);
330 this.protectedNomenclaturalTitleCache = protectedNomenclaturalTitleCache;
331 }
332
333 @Override
334 //@Transient //TODO a.kohlbecker remove??
335 public String getTitleCache() {
336 String result = "";
337 if (isProtectedTitleCache()){
338 result = this.titleCache;
339 }else{
340 result = generateTitle();
341 result = replaceEmptyTitleByNomTitle(result);
342 result = getTruncatedCache(result);
343 this.titleCache = result;
344 }
345 return result;
346 }
347
348 //#4311
349 @Override
350 public String getCollectorTitleCache(){
351 if (protectedCollectorTitleCache == PROTECTED){
352 return this.collectorTitleCache;
353 }
354 if (collectorTitleCache == null){
355 this.collectorTitleCache = getTruncatedCache(cacheStrategy().getCollectorTitleCache(this));
356 }else{
357 try {
358
359 //as long as team members do not inform the team about changes the cache must be created new each time
360 collectorTitleCache = getTruncatedCache(cacheStrategy().getCollectorTitleCache(this));
361 } catch (Exception e) {
362 //see #10090, getCollectorTitleCache is called e.g. if team is a secundum author, in this case if members are not initialized by BeanInitializer, however, jsonlib later tries to initialize the team (the exception is swallowed so it is not critical but a stacktrace is logged, which is not necessary)
363 //see also comment on TitleAndNameCacheAutoInitializer.initialize() for TaxonBase
364 if (logger.isDebugEnabled()){logger.debug("LIE during getCollectorTitleCache");}
365 return collectorTitleCache;
366 }
367 }
368 return collectorTitleCache;
369 }
370
371 /**
372 * Protected nomenclatural title cache flag should be set to true, if
373 * the title cache is to be preferred against the atomized data.
374 * This may be the case if no atomized data exists or if atomization
375 * was incomplete for whatever reason.
376 * @return
377 */
378 public boolean isProtectedNomenclaturalTitleCache() {
379 return protectedNomenclaturalTitleCache;
380 }
381 public void setProtectedNomenclaturalTitleCache(boolean protectedNomenclaturalTitleCache) {
382 this.protectedNomenclaturalTitleCache = protectedNomenclaturalTitleCache;
383 }
384
385
386 public boolean isProtectedCollectorTitleCache() {
387 return protectedCollectorTitleCache;
388 }
389 public void setProtectedCollectorTitleCache(boolean protectedCollectorTitleCache) {
390 this.protectedCollectorTitleCache = protectedCollectorTitleCache;
391 }
392
393 public void setCollectorTitleCache(String collectorTitleCache, boolean protectedCache) {
394 firePropertyChange("collectorTitleCache", this.collectorTitleCache, collectorTitleCache);
395 this.collectorTitleCache = CdmUtils.Nb(collectorTitleCache);
396 this.protectedCollectorTitleCache = protectedCache;
397 }
398
399 /**
400 * The hasMoreMembers flag is true if this team has more members than
401 * mentioned in the {@link #teamMembers} list. This is usually the case
402 * when "et al." is used in the team representation. Formatters should add
403 * "et al." or an according post fix to the team string representation
404 * if this flag is set.
405 * @return
406 */
407 public boolean isHasMoreMembers() {
408 return hasMoreMembers;
409 }
410 public void setHasMoreMembers(boolean hasMoreMembers) {
411 this.hasMoreMembers = hasMoreMembers;
412 }
413
414 @Override
415 public boolean updateCaches(){
416 boolean result = super.updateCaches();
417 if (this.protectedNomenclaturalTitleCache == false){
418 String oldNomTitleCache = this.nomenclaturalTitleCache;
419
420 String newNomTitleCache = cacheStrategy().getNomenclaturalTitleCache(this);
421
422 if ( oldNomTitleCache == null || ! oldNomTitleCache.equals(newNomTitleCache) ){
423 this.setNomenclaturalTitleCache(null, false);
424 String newCache = this.getNomenclaturalTitleCache();
425
426 if (newCache == null){
427 logger.warn("New nomTitleCache should never be null");
428 }
429 if (oldNomTitleCache == null){
430 logger.info("Old nomTitleCache should never be null");
431 }
432 result = true;
433 }
434 }
435 if (this.protectedCollectorTitleCache == false){
436 String oldCollTitleCache = this.collectorTitleCache;
437 String newCollTitleCache = cacheStrategy().getCollectorTitleCache(this);
438
439 if ( oldCollTitleCache == null || ! oldCollTitleCache.equals(newCollTitleCache) ){
440 this.setCollectorTitleCache(null, false);
441 String newCache = this.getCollectorTitleCache();
442
443 if (newCache == null){
444 logger.warn("New collectorTitleCache should never be null");
445 }
446 if (oldCollTitleCache == null){
447 logger.info("Old collectorTitleCache should never be null");
448 }
449 result = true;
450 }
451 }
452 return result;
453 }
454
455 //*********************** CLONE ********************************************************/
456
457 /**
458 * Clones <i>this</i> Team. This is a shortcut that enables to create
459 * a new instance that differs only slightly from <i>this</i> Team.
460 * The corresponding person is cloned.
461 *
462 * @see java.lang.Object#clone()
463 */
464 @Override
465 public Team clone() {
466 try{
467 Team result = (Team)super.clone();
468 result.teamMembers = new ArrayList<>();
469 for (Person teamMember: this.teamMembers){
470 result.addTeamMember(teamMember);
471 }
472 //no changes to protectedNomenclaturalTitleCache
473 return result;
474 } catch (CloneNotSupportedException e){
475 logger.warn("Object does not implement cloneable");
476 e.printStackTrace();
477 return null;
478 }
479 }
480 }