fix #7367 Evaluate configurator in isDeletable()
[cdmlib.git] / cdmlib-services / src / main / java / eu / etaxonomy / cdm / api / service / TermServiceImpl.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
10 package eu.etaxonomy.cdm.api.service;
11
12 import java.net.URI;
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.Enumeration;
16 import java.util.HashSet;
17 import java.util.List;
18 import java.util.Locale;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.UUID;
22
23 import org.apache.commons.lang.StringUtils;
24 import org.apache.log4j.Logger;
25 import org.springframework.beans.factory.annotation.Autowired;
26 import org.springframework.beans.factory.annotation.Qualifier;
27 import org.springframework.stereotype.Service;
28 import org.springframework.transaction.annotation.Transactional;
29
30 import eu.etaxonomy.cdm.api.service.UpdateResult.Status;
31 import eu.etaxonomy.cdm.api.service.config.DeleteConfiguratorBase;
32 import eu.etaxonomy.cdm.api.service.config.TermDeletionConfigurator;
33 import eu.etaxonomy.cdm.api.service.exception.DataChangeNoRollbackException;
34 import eu.etaxonomy.cdm.api.service.exception.ReferencedObjectUndeletableException;
35 import eu.etaxonomy.cdm.api.service.pager.Pager;
36 import eu.etaxonomy.cdm.api.service.pager.impl.DefaultPagerImpl;
37 import eu.etaxonomy.cdm.common.monitor.IProgressMonitor;
38 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
39 import eu.etaxonomy.cdm.model.common.CdmBase;
40 import eu.etaxonomy.cdm.model.common.DefinedTermBase;
41 import eu.etaxonomy.cdm.model.common.Language;
42 import eu.etaxonomy.cdm.model.common.LanguageString;
43 import eu.etaxonomy.cdm.model.common.LanguageStringBase;
44 import eu.etaxonomy.cdm.model.common.OrderedTermBase;
45 import eu.etaxonomy.cdm.model.common.OrderedTermVocabulary;
46 import eu.etaxonomy.cdm.model.common.Representation;
47 import eu.etaxonomy.cdm.model.common.TermType;
48 import eu.etaxonomy.cdm.model.common.TermVocabulary;
49 import eu.etaxonomy.cdm.model.location.NamedArea;
50 import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
51 import eu.etaxonomy.cdm.model.location.NamedAreaType;
52 import eu.etaxonomy.cdm.model.media.Media;
53 import eu.etaxonomy.cdm.persistence.dao.common.IDefinedTermDao;
54 import eu.etaxonomy.cdm.persistence.dao.common.ILanguageStringBaseDao;
55 import eu.etaxonomy.cdm.persistence.dao.common.ILanguageStringDao;
56 import eu.etaxonomy.cdm.persistence.dao.common.IRepresentationDao;
57 import eu.etaxonomy.cdm.persistence.dto.TermDto;
58 import eu.etaxonomy.cdm.persistence.dto.UuidAndTitleCache;
59 import eu.etaxonomy.cdm.persistence.query.OrderHint;
60 import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
61
62 @Service
63 @Transactional(readOnly = true)
64 public class TermServiceImpl extends IdentifiableServiceBase<DefinedTermBase,IDefinedTermDao> implements ITermService{
65 @SuppressWarnings("unused")
66 private static final Logger logger = Logger.getLogger(TermServiceImpl.class);
67
68 private ILanguageStringDao languageStringDao;
69
70 @Autowired
71 private IVocabularyService vocabularyService;
72
73 @Autowired
74 @Qualifier("langStrBaseDao")
75 private ILanguageStringBaseDao languageStringBaseDao;
76 private IRepresentationDao representationDao;
77
78 @Autowired
79 public void setLanguageStringDao(ILanguageStringDao languageStringDao) {
80 this.languageStringDao = languageStringDao;
81 }
82
83 @Autowired
84 public void setRepresentationDao(IRepresentationDao representationDao) {
85 this.representationDao = representationDao;
86 }
87
88 @Override
89 @Autowired
90 protected void setDao(IDefinedTermDao dao) {
91 this.dao = dao;
92 }
93
94 @Override
95 public <T extends DefinedTermBase> List<T> listByTermType(TermType termType, Integer limit, Integer start,
96 List<OrderHint> orderHints, List<String> propertyPaths) {
97 return dao.listByTermType(termType, limit, start, orderHints, propertyPaths);
98 }
99
100 @Override
101 public DefinedTermBase getByUri(URI uri) {
102 return dao.findByUri(uri);
103 }
104
105 @Override
106 public Language getLanguageByIso(String iso639) {
107 return dao.getLanguageByIso(iso639);
108 }
109
110 @Override
111 public Language getLanguageByLabel(String label) {
112 return Language.getLanguageByLabel(label);
113 }
114
115 @Override
116 public List<Language> getLanguagesByLocale(Enumeration<Locale> locales){
117 return dao.getLanguagesByLocale(locales);
118 }
119
120 @Override
121 public <TERM extends DefinedTermBase> TERM findByIdInVocabulary(String id, UUID vocabularyUuid, Class<TERM> clazz) throws IllegalArgumentException {
122 List<TERM> list = dao.getDefinedTermByIdInVocabulary(id, vocabularyUuid, clazz, null, null);
123 if (list.isEmpty()){
124 return null;
125 }else if (list.size() == 1){
126 return list.get(0);
127 }else{
128 String message = "There is more then 1 (%d) term with the same id in vocabulary. This is forbidden. Check the state of your database.";
129 throw new IllegalStateException(String.format(message, list.size()));
130 }
131 }
132
133
134 @Override
135 public NamedArea getAreaByTdwgAbbreviation(String tdwgAbbreviation) {
136 if (StringUtils.isBlank(tdwgAbbreviation)){ //TDWG areas should always have a label
137 return null;
138 }
139 List<NamedArea> list = dao.getDefinedTermByIdInVocabulary(tdwgAbbreviation, NamedArea.uuidTdwgAreaVocabulary, NamedArea.class, null, null);
140 if (list.isEmpty()){
141 return null;
142 }else if (list.size() == 1){
143 return list.get(0);
144 }else{
145 String message = "There is more then 1 (%d) TDWG area with the same abbreviated label. This is forbidden. Check the state of your database.";
146 throw new IllegalStateException(String.format(message, list.size()));
147 }
148 }
149
150 @Override
151 public <T extends DefinedTermBase> Pager<T> getGeneralizationOf(T definedTerm, Integer pageSize, Integer pageNumber) {
152 long numberOfResults = dao.countGeneralizationOf(definedTerm);
153
154 List<T> results = new ArrayList<>();
155 if(numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
156 results = dao.getGeneralizationOf(definedTerm, pageSize, pageNumber);
157 }
158
159 return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
160 }
161
162 @Override
163 public <T extends DefinedTermBase> Pager<T> getIncludes(Collection<T> definedTerms, Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
164 long numberOfResults = dao.countIncludes(definedTerms);
165
166 List<T> results = new ArrayList<>();
167 if(numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
168 results = dao.getIncludes(definedTerms, pageSize, pageNumber,propertyPaths);
169 }
170
171 return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
172 }
173
174 @Override
175 public Pager<Media> getMedia(DefinedTermBase definedTerm, Integer pageSize, Integer pageNumber) {
176 long numberOfResults = dao.countMedia(definedTerm);
177
178 List<Media> results = new ArrayList<>();
179 if(numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
180 results = dao.getMedia(definedTerm, pageSize, pageNumber);
181 }
182
183 return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
184 }
185
186 @Override
187 public <T extends DefinedTermBase> Pager<T> getPartOf(Set<T> definedTerms,Integer pageSize, Integer pageNumber, List<String> propertyPaths) {
188 long numberOfResults = dao.countPartOf(definedTerms);
189
190 List<T> results = new ArrayList<>();
191 if(numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
192 results = dao.getPartOf(definedTerms, pageSize, pageNumber, propertyPaths);
193 }
194
195 return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
196 }
197
198 @Override
199 public Pager<NamedArea> list(NamedAreaLevel level, NamedAreaType type, Integer pageSize, Integer pageNumber,
200 List<OrderHint> orderHints, List<String> propertyPaths) {
201 long numberOfResults = dao.count(level, type);
202
203 List<NamedArea> results = new ArrayList<>();
204 if (numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
205 results = dao.list(level, type, pageSize, pageNumber, orderHints, propertyPaths);
206 }
207
208 return new DefaultPagerImpl<>(pageNumber, numberOfResults, pageSize, results);
209 }
210
211 @Override
212 public <T extends DefinedTermBase> Pager<T> findByRepresentationText(String label, Class<T> clazz, Integer pageSize, Integer pageNumber) {
213 long numberOfResults = dao.countDefinedTermByRepresentationText(label,clazz);
214
215 List<T> results = new ArrayList<>();
216 if(numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
217 results = dao.getDefinedTermByRepresentationText(label, clazz, pageSize, pageNumber);
218 }
219
220 return new DefaultPagerImpl<T>(pageNumber, numberOfResults, pageSize, results);
221 }
222
223 @Override
224 public <T extends DefinedTermBase> Pager<T> findByRepresentationAbbreviation(String abbrev, Class<T> clazz, Integer pageSize, Integer pageNumber) {
225 long numberOfResults = dao.countDefinedTermByRepresentationAbbrev(abbrev,clazz);
226
227 List<T> results = new ArrayList<>();
228 if(numberOfResults > 0) { // no point checking again //TODO use AbstractPagerImpl.hasResultsInRange(numberOfResults, pageNumber, pageSize)
229 results = dao.getDefinedTermByRepresentationAbbrev(abbrev, clazz, pageSize, pageNumber);
230 }
231
232 return new DefaultPagerImpl<T>(pageNumber, numberOfResults, pageSize, results);
233 }
234
235 @Override
236 public List<LanguageString> getAllLanguageStrings(int limit, int start) {
237 return languageStringDao.list(limit, start);
238 }
239
240 @Override
241 public List<Representation> getAllRepresentations(int limit, int start) {
242 return representationDao.list(limit,start);
243 }
244
245 @Override
246 public UUID saveLanguageData(LanguageStringBase languageData) {
247 return languageStringBaseDao.save(languageData).getUuid();
248 }
249
250
251 /** @deprecated use {@link #delete(DefinedTermBase, TermDeletionConfigurator)} instead
252 * to allow DeleteResult return type*/
253 @Override
254 @Deprecated
255 public DeleteResult delete(DefinedTermBase term){
256 DeleteResult result = new DeleteResult();
257
258 TermDeletionConfigurator defaultConfig = new TermDeletionConfigurator();
259 result = delete(term, defaultConfig);
260 return result;
261 }
262
263 @Override
264 @Deprecated
265 @Transactional(readOnly = false)
266 public DeleteResult delete(UUID termUuid){
267 DeleteResult result = new DeleteResult();
268
269 TermDeletionConfigurator defaultConfig = new TermDeletionConfigurator();
270 result = delete(dao.load(termUuid), defaultConfig);
271 return result;
272 }
273
274 @Override
275 public DeleteResult delete(DefinedTermBase term, TermDeletionConfigurator config){
276 if (config == null){
277 config = new TermDeletionConfigurator();
278 }
279 Set<DefinedTermBase> termsToSave = new HashSet<DefinedTermBase>();
280
281 DeleteResult result = isDeletable(term.getUuid(), config);
282 try {
283 //generalization of
284 Set<DefinedTermBase> specificTerms = term.getGeneralizationOf();
285 if (specificTerms.size()>0){
286 if (config.isDeleteGeneralizationOfRelations()){
287 DefinedTermBase generalTerm = term.getKindOf();
288 for (DefinedTermBase specificTerm: specificTerms){
289 term.removeGeneralization(specificTerm);
290 if (generalTerm != null){
291 generalTerm.addGeneralizationOf(specificTerm);
292 termsToSave.add(generalTerm);
293 }
294 }
295 }else{
296 //TODO Exception type
297 String message = "This term has specifing terms. Move or delete specifiing terms prior to delete or change delete configuration.";
298 result.addRelatedObjects(specificTerms);
299 result.setAbort();
300 Exception ex = new DataChangeNoRollbackException(message);
301 result.addException(ex);
302 }
303 }
304
305 //kind of
306 DefinedTermBase generalTerm = term.getKindOf();
307 if (generalTerm != null){
308 if (config.isDeleteKindOfRelations()){
309 generalTerm.removeGeneralization(term);
310 }else{
311 //TODO Exception type
312 String message = "This term is kind of another term. Move or delete kind of relationship prior to delete or change delete configuration.";
313 result.addRelatedObject(generalTerm);
314 result.setAbort();
315 DataChangeNoRollbackException ex = new DataChangeNoRollbackException(message);
316 result.addException(ex);
317 throw ex;
318 }
319 }
320
321 //part of
322 DefinedTermBase parentTerm = term.getPartOf();
323 if (parentTerm != null){
324 if (! config.isDeletePartOfRelations()){
325 //TODO Exception type
326 String message = "This term is included in another term. Remove from parent term prior to delete or change delete configuration.";
327 result.addRelatedObject(parentTerm);
328 result.setAbort();
329 DataChangeNoRollbackException ex = new DataChangeNoRollbackException(message);
330 result.addException(ex);
331 }
332 }
333
334
335 //included in
336 Set<DefinedTermBase> includedTerms = term.getIncludes();
337 if (includedTerms.size()> 0){
338 if (config.isDeleteIncludedRelations()){
339 DefinedTermBase parent = term.getPartOf();
340 for (DefinedTermBase includedTerm: includedTerms){
341 term.removeIncludes(includedTerm);
342 if (parent != null){
343 parent.addIncludes(includedTerm);
344 termsToSave.add(parent);
345 }
346 }
347 }else{
348 //TODO Exception type
349 String message = "This term includes other terms. Move or delete included terms prior to delete or change delete configuration.";
350 result.addRelatedObjects(includedTerms);
351 result.setAbort();
352 Exception ex = new DataChangeNoRollbackException(message);
353 result.addException(ex);
354 }
355 }
356
357 //part of
358 if (parentTerm != null){
359 if (config.isDeletePartOfRelations()){
360 parentTerm.removeIncludes(term);
361 termsToSave.add(parentTerm);
362 }else{
363 //handled before "included in"
364 }
365 }
366
367 if (result.isOk()){
368 TermVocabulary voc = term.getVocabulary();
369 if (voc!= null){
370 voc.removeTerm(term);
371 }
372 //TODO save voc
373 if (true){
374 dao.delete(term);
375 result.addDeletedObject(term);
376 dao.saveOrUpdateAll(termsToSave);
377 }
378 }
379 } catch (DataChangeNoRollbackException e) {
380 result.setStatus(Status.ERROR);
381 }
382 return result;
383 }
384
385 @Override
386 @Transactional(readOnly = false)
387 public DeleteResult delete(UUID termUuid, TermDeletionConfigurator config){
388 return delete(dao.load(termUuid), config);
389 }
390
391 @Override
392 @Transactional(readOnly = false)
393 public void updateTitleCache(Class<? extends DefinedTermBase> clazz, Integer stepSize, IIdentifiableEntityCacheStrategy<DefinedTermBase> cacheStrategy, IProgressMonitor monitor) {
394 //TODO shouldnt this be TermBase instead of DefinedTermBase
395 if (clazz == null){
396 clazz = DefinedTermBase.class;
397 }
398 super.updateTitleCacheImpl(clazz, stepSize, cacheStrategy, monitor);
399 }
400
401 @Override
402 public DeleteResult isDeletable(UUID termUuid, DeleteConfiguratorBase config){
403 TermDeletionConfigurator termConfig = null;
404 if(config instanceof TermDeletionConfigurator){
405 termConfig = (TermDeletionConfigurator) config;
406 }
407 DeleteResult result = new DeleteResult();
408 DefinedTermBase term = load(termUuid);
409
410 if(termConfig!=null){
411 //generalization of
412 Set<DefinedTermBase> specificTerms = term.getGeneralizationOf();
413 if (!specificTerms.isEmpty() && !termConfig.isDeleteGeneralizationOfRelations()){
414 result.getRelatedObjects().addAll(specificTerms);
415 result.setAbort();
416 }
417 //kind of
418 DefinedTermBase generalTerm = term.getKindOf();
419 if (generalTerm != null && !termConfig.isDeleteKindOfRelations()){
420 result.setAbort();
421 result.getRelatedObjects().add(generalTerm);
422 }
423 //part of
424 DefinedTermBase parentTerm = term.getPartOf();
425 if (parentTerm != null && !termConfig.isDeletePartOfRelations()){
426 result.setAbort();
427 result.getRelatedObjects().add(parentTerm);
428 }
429 //included in
430 Set<DefinedTermBase> includedTerms = term.getIncludes();
431 if (!includedTerms.isEmpty() && !termConfig.isDeleteIncludedRelations()){
432 result.setAbort();
433 result.getRelatedObjects().addAll(includedTerms);
434 }
435 }
436
437 //gather remaining referenced objects
438 Set<CdmBase> references = commonService.getReferencingObjectsForDeletion(term);
439 for (CdmBase cdmBase : references) {
440 if(result.getRelatedObjects().contains(cdmBase)){
441 result.getRelatedObjects().remove(cdmBase);
442 }
443 }
444 result.getRelatedObjects().forEach(relatedObject->{
445 String message = "An object of " + relatedObject.getClass().getName() + " with ID " + relatedObject.getId() + " is referencing the object" ;
446 result.addException(new ReferencedObjectUndeletableException(message));
447 result.setAbort();
448 });
449 return result;
450 }
451
452 @Override
453 @Transactional(readOnly = false)
454 public Map<UUID, Representation> saveOrUpdateRepresentations(Collection<Representation> representations){
455 return representationDao.saveOrUpdateAll(representations);
456 }
457
458 @Override
459 @Transactional(readOnly = true)
460 public List<UuidAndTitleCache<NamedArea>> getUuidAndTitleCache(List<TermVocabulary> vocs, Integer limit, String pattern, Language lang) {
461 List<NamedArea> areas = dao.getUuidAndTitleCache(vocs, limit, pattern);
462
463 List<UuidAndTitleCache<NamedArea>> result = new ArrayList();
464 UuidAndTitleCache<NamedArea> uuidAndTitleCache;
465 for (NamedArea area: areas){
466 uuidAndTitleCache = new UuidAndTitleCache<>(area.getUuid(), area.getId(), area.labelWithLevel(area, lang));
467 result.add(uuidAndTitleCache);
468 }
469
470 return result;
471 }
472
473 @Override
474 public Collection<TermDto> getIncludesAsDto(
475 TermDto parentTerm) {
476 return dao.getIncludesAsDto(parentTerm);
477 }
478
479 @Override
480 public Collection<TermDto> getKindOfsAsDto(
481 TermDto parentTerm) {
482 return dao.getKindOfsAsDto(parentTerm);
483 }
484
485 @Transactional(readOnly = false)
486 @Override
487 public void moveTerm(TermDto termDto, UUID parentUUID) {
488 moveTerm(termDto, parentUUID, null);
489 }
490
491 @SuppressWarnings({ "rawtypes", "unchecked" })
492 @Transactional(readOnly = false)
493 @Override
494 public void moveTerm(TermDto termDto, UUID parentUuid, TermMovePosition termMovePosition) {
495 boolean isKindOf = termDto.getKindOfUuid()!=null && termDto.getKindOfUuid().equals(parentUuid);
496 TermVocabulary vocabulary = HibernateProxyHelper.deproxy(vocabularyService.load(termDto.getVocabularyUuid()));
497 DefinedTermBase parent = HibernateProxyHelper.deproxy(dao.load(parentUuid));
498 if(parent==null){
499 //new parent is a vocabulary
500 TermVocabulary parentVocabulary = HibernateProxyHelper.deproxy(vocabularyService.load(parentUuid));
501 DefinedTermBase term = HibernateProxyHelper.deproxy(dao.load(termDto.getUuid()));
502 if(parentVocabulary!=null){
503 term.setKindOf(null);
504 term.setPartOf(null);
505
506 vocabulary.removeTerm(term);
507 parentVocabulary.addTerm(term);
508 }
509 vocabularyService.saveOrUpdate(parentVocabulary);
510 }
511 else {
512 DefinedTermBase term = HibernateProxyHelper.deproxy(dao.load(termDto.getUuid()));
513 //new parent is a term
514 if(parent.isInstanceOf(OrderedTermBase.class)
515 && term.isInstanceOf(OrderedTermBase.class)
516 && termMovePosition!=null
517 && HibernateProxyHelper.deproxy(parent, OrderedTermBase.class).getVocabulary().isInstanceOf(OrderedTermVocabulary.class)) {
518 //new parent is an ordered term
519 OrderedTermBase orderedTerm = HibernateProxyHelper.deproxy(term, OrderedTermBase.class);
520 OrderedTermBase targetOrderedDefinedTerm = HibernateProxyHelper.deproxy(parent, OrderedTermBase.class);
521 OrderedTermVocabulary otVoc = HibernateProxyHelper.deproxy(targetOrderedDefinedTerm.getVocabulary(), OrderedTermVocabulary.class);
522 if(termMovePosition.equals(TermMovePosition.BEFORE)) {
523 orderedTerm.getVocabulary().removeTerm(orderedTerm);
524 otVoc.addTermAbove(orderedTerm, targetOrderedDefinedTerm);
525 if (targetOrderedDefinedTerm.getPartOf() != null){
526 targetOrderedDefinedTerm.getPartOf().addIncludes(orderedTerm);
527 }
528 }
529 else if(termMovePosition.equals(TermMovePosition.AFTER)) {
530 orderedTerm.getVocabulary().removeTerm(orderedTerm);
531 otVoc.addTermBelow(orderedTerm, targetOrderedDefinedTerm);
532 if (targetOrderedDefinedTerm.getPartOf() != null){
533 targetOrderedDefinedTerm.getPartOf().addIncludes(orderedTerm);
534 }
535 }
536 else if(termMovePosition.equals(TermMovePosition.ON)) {
537 orderedTerm.getVocabulary().removeTerm(orderedTerm);
538 targetOrderedDefinedTerm.addIncludes(orderedTerm);
539 targetOrderedDefinedTerm.getVocabulary().addTerm(orderedTerm);
540 }
541 }
542 else{
543 vocabulary.removeTerm(term);
544 if(isKindOf){
545 parent.addGeneralizationOf(term);
546 }
547 else{
548 parent.addIncludes(term);
549 }
550 parent.getVocabulary().addTerm(term);
551 }
552 vocabularyService.saveOrUpdate(parent.getVocabulary());
553 }
554 }
555
556 @SuppressWarnings({ "rawtypes", "unchecked" })
557 @Transactional(readOnly = false)
558 @Override
559 public TermDto addNewTerm(TermType termType, UUID parentUUID, boolean isKindOf) {
560 DefinedTermBase term = termType.getEmptyDefinedTermBase();
561 dao.save(term);
562 DefinedTermBase parent = dao.load(parentUUID);
563 if(isKindOf){
564 parent.addGeneralizationOf(term);
565 }
566 else{
567 parent.addIncludes(term);
568 }
569 parent.getVocabulary().addTerm(term);
570 dao.saveOrUpdate(parent);
571 return TermDto.fromTerm(term, true);
572 }
573
574 public enum TermMovePosition{
575 BEFORE,
576 AFTER,
577 ON
578 }
579
580 }