Project

General

Profile

« Previous | Next » 

Revision 97ee0157

Added by Andreas Müller over 8 years ago

Adapt listeners and tests to new pre-insert strategy #5066

View differences:

cdmlib-persistence/src/main/java/eu/etaxonomy/cdm/persistence/hibernate/CacheStrategyGenerator.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.persistence.hibernate;
11

  
12
import java.util.Map;
13

  
14
import org.apache.log4j.Logger;
15
import org.hibernate.HibernateException;
16
import org.hibernate.event.spi.MergeEvent;
17
import org.hibernate.event.spi.MergeEventListener;
18
import org.hibernate.event.spi.SaveOrUpdateEvent;
19
import org.hibernate.event.spi.SaveOrUpdateEventListener;
20

  
21
import eu.etaxonomy.cdm.model.agent.Team;
22
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
23
import eu.etaxonomy.cdm.model.common.CdmBase;
24
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
25
import eu.etaxonomy.cdm.model.molecular.Amplification;
26
import eu.etaxonomy.cdm.model.name.NonViralName;
27
import eu.etaxonomy.cdm.model.reference.Reference;
28

  
29
/**
30
 * @author a.mueller
31
 * @created 04.03.2009
32
 */
33
public class CacheStrategyGenerator implements SaveOrUpdateEventListener, MergeEventListener {
34
    private static final long serialVersionUID = -5511287200489449838L;
35
    @SuppressWarnings("unused")
36
    private static final Logger logger = Logger.getLogger(CacheStrategyGenerator.class);
37

  
38
    @Override
39
    public void onSaveOrUpdate(SaveOrUpdateEvent event) throws HibernateException {
40
        Object entity = event.getObject();
41
        saveOrUpdateOrMerge(entity);
42
    }
43

  
44
    /* (non-Javadoc)
45
     * @see org.hibernate.event.spi.MergeEventListener#onMerge(org.hibernate.event.spi.MergeEvent)
46
     */
47
    @Override
48
    public void onMerge(MergeEvent event) throws HibernateException {
49
        Object entity = event.getOriginal();
50
        saveOrUpdateOrMerge(entity);
51
    }
52

  
53
    /* (non-Javadoc)
54
     * @see org.hibernate.event.spi.MergeEventListener#onMerge(org.hibernate.event.spi.MergeEvent, java.util.Map)
55
     */
56
    @Override
57
    public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException {
58

  
59
    }
60

  
61
    private void saveOrUpdateOrMerge(Object entity) {
62
        if (entity != null){
63
            Class<?> entityClazz = entity.getClass();
64

  
65
            //non-viral-name caches
66
            if(NonViralName.class.isAssignableFrom(entityClazz)) {
67
                NonViralName<?> nonViralName = (NonViralName<?>)entity;
68
                nonViralName.getAuthorshipCache();
69
                nonViralName.getNameCache();
70
                nonViralName.getTitleCache();
71
                nonViralName.getFullTitleCache();
72
                //team-or-person caches
73
            }else if(TeamOrPersonBase.class.isAssignableFrom(entityClazz)){
74
                TeamOrPersonBase<?> teamOrPerson = (TeamOrPersonBase<?>)entity;
75
                String nomTitle = teamOrPerson.getNomenclaturalTitle();
76
                if (teamOrPerson instanceof Team){
77
                    Team team =CdmBase.deproxy(teamOrPerson, Team.class);
78
                    team.setNomenclaturalTitle(nomTitle, team.isProtectedNomenclaturalTitleCache()); //nomTitle is not necessarily cached when it is created
79
                }else{
80
                    teamOrPerson.setNomenclaturalTitle(nomTitle);
81
                }
82
                String titleCache = teamOrPerson.getTitleCache();
83
                if (! teamOrPerson.isProtectedTitleCache()){
84
                    teamOrPerson.setTitleCache(titleCache, false);
85
                }
86

  
87
                //reference caches
88
            }else if(Reference.class.isAssignableFrom(entityClazz)){
89
                Reference<?> ref = (Reference<?>)entity;
90
                ref.getAbbrevTitleCache();
91
                ref.getTitleCache();
92
                //title cache
93
            }else if(IdentifiableEntity.class.isAssignableFrom(entityClazz)) {
94
                IdentifiableEntity<?> identifiableEntity = (IdentifiableEntity)entity;
95
                identifiableEntity.getTitleCache();
96
            }else if(Amplification.class.isAssignableFrom(entityClazz)) {
97
                Amplification amplification = (Amplification)entity;
98
                amplification.updateCache();
99
            }
100

  
101
        }
102
    }
103
}
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.persistence.hibernate;
11

  
12
import java.util.Map;
13

  
14
import org.apache.log4j.Logger;
15
import org.hibernate.HibernateException;
16
import org.hibernate.event.spi.MergeEvent;
17
import org.hibernate.event.spi.MergeEventListener;
18
import org.hibernate.event.spi.SaveOrUpdateEvent;
19
import org.hibernate.event.spi.SaveOrUpdateEventListener;
20

  
21
/**
22
 * @author a.mueller
23
 * @created 04.03.2009
24
 */
25
public class CacheStrategyGenerator implements SaveOrUpdateEventListener, MergeEventListener {
26
    private static final long serialVersionUID = -5511287200489449838L;
27
    @SuppressWarnings("unused")
28
    private static final Logger logger = Logger.getLogger(CacheStrategyGenerator.class);
29

  
30
    @Override
31
    public void onSaveOrUpdate(SaveOrUpdateEvent event) throws HibernateException {
32
        Object entity = event.getObject();
33
        saveOrUpdateOrMerge(entity);
34
    }
35

  
36
    @Override
37
    public void onMerge(MergeEvent event) throws HibernateException {
38
        Object entity = event.getOriginal();
39
        saveOrUpdateOrMerge(entity);
40
    }
41

  
42
    @Override
43
    public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException {
44

  
45
    }
46

  
47
    private void saveOrUpdateOrMerge(Object entity) {
48
        CdmPreDataChangeObservableListener.generateCaches(entity);
49
        //moved to CdmPreDataChangeObservableListener
50
//        if (entity != null){
51
//            Class<?> entityClazz = entity.getClass();
52
//
53
//            //non-viral-name caches
54
//            if(NonViralName.class.isAssignableFrom(entityClazz)) {
55
//                NonViralName<?> nonViralName = (NonViralName<?>)entity;
56
//                nonViralName.getAuthorshipCache();
57
//                nonViralName.getNameCache();
58
//                nonViralName.getTitleCache();
59
//                nonViralName.getFullTitleCache();
60
//                //team-or-person caches
61
//            }else if(TeamOrPersonBase.class.isAssignableFrom(entityClazz)){
62
//                TeamOrPersonBase<?> teamOrPerson = (TeamOrPersonBase<?>)entity;
63
//                String nomTitle = teamOrPerson.getNomenclaturalTitle();
64
//                if (teamOrPerson instanceof Team){
65
//                    Team team =CdmBase.deproxy(teamOrPerson, Team.class);
66
//                    team.setNomenclaturalTitle(nomTitle, team.isProtectedNomenclaturalTitleCache()); //nomTitle is not necessarily cached when it is created
67
//                }else{
68
//                    teamOrPerson.setNomenclaturalTitle(nomTitle);
69
//                }
70
//                String titleCache = teamOrPerson.getTitleCache();
71
//                if (! teamOrPerson.isProtectedTitleCache()){
72
//                    teamOrPerson.setTitleCache(titleCache, false);
73
//                }
74
//
75
//                //reference caches
76
//            }else if(Reference.class.isAssignableFrom(entityClazz)){
77
//                Reference<?> ref = (Reference<?>)entity;
78
//                ref.getAbbrevTitleCache();
79
//                ref.getTitleCache();
80
//                //title cache
81
//            }else if(IdentifiableEntity.class.isAssignableFrom(entityClazz)) {
82
//                IdentifiableEntity<?> identifiableEntity = (IdentifiableEntity)entity;
83
//                identifiableEntity.getTitleCache();
84
//            }else if(Amplification.class.isAssignableFrom(entityClazz)) {
85
//                Amplification amplification = (Amplification)entity;
86
//                amplification.updateCache();
87
//            }
88
//
89
//        }
90
    }
91
}
cdmlib-persistence/src/main/java/eu/etaxonomy/cdm/persistence/hibernate/CdmListenerIntegrator.java
1
/**
2
 *
3
 */
4
package eu.etaxonomy.cdm.persistence.hibernate;
5

  
6
import org.apache.log4j.Logger;
7
import org.hibernate.cfg.Configuration;
8
import org.hibernate.engine.spi.SessionFactoryImplementor;
9
import org.hibernate.event.service.spi.EventListenerRegistry;
10
import org.hibernate.event.spi.EventType;
11
import org.hibernate.integrator.spi.Integrator;
12
import org.hibernate.metamodel.source.MetadataImplementor;
13
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
14

  
15
/**
16
 * @author a.mueller
17
 * @created 30.03.2013
18
 *
19
 */
20
public class CdmListenerIntegrator implements Integrator {
21
	private static final Logger logger = Logger.getLogger(CdmListenerIntegrator.class);
22

  
23

  
24
	/*
25
	 * (non-Javadoc)
26
	 *
27
	 * @see org.hibernate.integrator.spi.Integrator#integrate(org.hibernate.cfg.Configuration,
28
	 * org.hibernate.engine.spi.SessionFactoryImplementor,
29
	 * org.hibernate.service.spi.SessionFactoryServiceRegistry)
30
	 */
31
	@Override
32
	public void integrate(Configuration configuration, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry){
33
		if (logger.isInfoEnabled()) {
34
			logger.info("Registering event listeners");
35
		}
36

  
37
		final EventListenerRegistry eventRegistry = serviceRegistry.getService(EventListenerRegistry.class);
38

  
39
		//duplication strategy
40
		eventRegistry.addDuplicationStrategy(CdmListenerDuplicationStrategy.NewInstance);
41

  
42
//		ValidationExecutor validationExecutor = new ValidationExecutor();
43
//		Level2ValidationEventListener l2Listener = new Level2ValidationEventListener();
44
//		l2Listener.setValidationExecutor(validationExecutor);
45
//		Level3ValidationEventListener l3Listener = new Level3ValidationEventListener();
46
//		l3Listener.setValidationExecutor(validationExecutor);
47

  
48
		// prepend to register before or append to register after
49
		// this example will register a persist event listener
50
		//eventRegistry.prependListeners(EventType.SAVE, new CacheStrategyGenerator(), new SaveEntityListener());
51
		eventRegistry.prependListeners(EventType.UPDATE, new CacheStrategyGenerator(), new UpdateEntityListener());
52

  
53
		eventRegistry.prependListeners(EventType.SAVE_UPDATE, new CacheStrategyGenerator(), new SaveOrUpdateorMergeEntityListener());
54
		eventRegistry.prependListeners(EventType.MERGE, new CacheStrategyGenerator(), new SaveOrUpdateorMergeEntityListener());
55

  
56
//		eventRegistry.prependListeners(EventType.SAVE_UPDATE, new CacheStrategyGenerator(), new SaveOrUpdateEntityListener());
57
		//TODO Cherian move to SaveOrUpdateorMergeEntityListener
58
		eventRegistry.appendListeners(EventType.MERGE, new PostMergeEntityListener());
59

  
60
		eventRegistry.appendListeners(EventType.DELETE, new CdmDeleteListener());
61
		eventRegistry.appendListeners(EventType.POST_LOAD, new CdmPostDataChangeObservableListener());
62
//with validation
63
//		eventRegistry.appendListeners(EventType.POST_INSERT, new CdmPostDataChangeObservableListener(), l2Listener, l3Listener);
64
//		eventRegistry.appendListeners(EventType.POST_UPDATE, new CdmPostDataChangeObservableListener(), l2Listener, l3Listener);
65
//		eventRegistry.appendListeners(EventType.POST_DELETE, new CdmPostDataChangeObservableListener(), l3Listener);
66
//without validation
67
		eventRegistry.appendListeners(EventType.POST_INSERT, new CdmPostDataChangeObservableListener());
68
		eventRegistry.appendListeners(EventType.POST_UPDATE, new CdmPostDataChangeObservableListener());
69
		eventRegistry.appendListeners(EventType.POST_DELETE, new CdmPostDataChangeObservableListener());
70

  
71
		eventRegistry.appendListeners(EventType.PRE_INSERT, new CdmPreDataChangeObservableListener());
72
		eventRegistry.appendListeners(EventType.PRE_UPDATE, new CdmPreDataChangeObservableListener());
73
	}
74

  
75

  
76
	/*
77
	 * (non-Javadoc)
78
	 *
79
	 * @see org.hibernate.integrator.spi.Integrator#integrate(org.hibernate.metamodel.source.
80
	 * MetadataImplementor, org.hibernate.engine.spi.SessionFactoryImplementor,
81
	 * org.hibernate.service.spi.SessionFactoryServiceRegistry)
82
	 */
83
	@Override
84
	public void integrate(MetadataImplementor metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry){
85
		//nothing to do for now
86
		logger.warn("Metadata integrate not yet implemented");
87
	}
88

  
89

  
90
	/*
91
	 * (non-Javadoc)
92
	 *
93
	 * @see org.hibernate.integrator.spi.Integrator#disintegrate(org.hibernate.engine.spi.
94
	 * SessionFactoryImplementor, org.hibernate.service.spi.SessionFactoryServiceRegistry)
95
	 */
96
	@Override
97
	public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry)
98
	{
99
		//nothing to do for now
100
		logger.warn("Disintegrate not yet implemented");
101
	}
102

  
103
}
1
/**
2
 *
3
 */
4
package eu.etaxonomy.cdm.persistence.hibernate;
5

  
6
import org.apache.log4j.Logger;
7
import org.hibernate.cfg.Configuration;
8
import org.hibernate.engine.spi.SessionFactoryImplementor;
9
import org.hibernate.event.service.spi.EventListenerRegistry;
10
import org.hibernate.event.spi.EventType;
11
import org.hibernate.integrator.spi.Integrator;
12
import org.hibernate.metamodel.source.MetadataImplementor;
13
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
14

  
15
/**
16
 * @author a.mueller
17
 * @created 30.03.2013
18
 *
19
 */
20
public class CdmListenerIntegrator implements Integrator {
21
	private static final Logger logger = Logger.getLogger(CdmListenerIntegrator.class);
22

  
23

  
24
	/*
25
	 * (non-Javadoc)
26
	 *
27
	 * @see org.hibernate.integrator.spi.Integrator#integrate(org.hibernate.cfg.Configuration,
28
	 * org.hibernate.engine.spi.SessionFactoryImplementor,
29
	 * org.hibernate.service.spi.SessionFactoryServiceRegistry)
30
	 */
31
	@Override
32
	public void integrate(Configuration configuration, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry){
33
		if (logger.isInfoEnabled()) {
34
			logger.info("Registering event listeners");
35
		}
36

  
37
		final EventListenerRegistry eventRegistry = serviceRegistry.getService(EventListenerRegistry.class);
38

  
39
		//duplication strategy
40
		eventRegistry.addDuplicationStrategy(CdmListenerDuplicationStrategy.NewInstance);
41

  
42
//		ValidationExecutor validationExecutor = new ValidationExecutor();
43
//		Level2ValidationEventListener l2Listener = new Level2ValidationEventListener();
44
//		l2Listener.setValidationExecutor(validationExecutor);
45
//		Level3ValidationEventListener l3Listener = new Level3ValidationEventListener();
46
//		l3Listener.setValidationExecutor(validationExecutor);
47

  
48
		// prepend to register before or append to register after
49
		// this example will register a persist event listener
50
		eventRegistry.prependListeners(EventType.SAVE, new CacheStrategyGenerator(), new SaveEntityListener());
51
		eventRegistry.prependListeners(EventType.UPDATE, new CacheStrategyGenerator(), new UpdateEntityListener());
52

  
53
		eventRegistry.prependListeners(EventType.SAVE_UPDATE, new CacheStrategyGenerator(), new SaveOrUpdateorMergeEntityListener());
54
		eventRegistry.prependListeners(EventType.MERGE, new CacheStrategyGenerator(), new SaveOrUpdateorMergeEntityListener());
55

  
56
//		eventRegistry.prependListeners(EventType.SAVE_UPDATE, new CacheStrategyGenerator(), new SaveOrUpdateEntityListener());
57
		//TODO Cherian move to SaveOrUpdateorMergeEntityListener
58
		eventRegistry.appendListeners(EventType.MERGE, new PostMergeEntityListener());
59

  
60
		eventRegistry.appendListeners(EventType.DELETE, new CdmDeleteListener());
61
		eventRegistry.appendListeners(EventType.POST_LOAD, new CdmPostDataChangeObservableListener());
62
//with validation
63
//		eventRegistry.appendListeners(EventType.POST_INSERT, new CdmPostDataChangeObservableListener(), l2Listener, l3Listener);
64
//		eventRegistry.appendListeners(EventType.POST_UPDATE, new CdmPostDataChangeObservableListener(), l2Listener, l3Listener);
65
//		eventRegistry.appendListeners(EventType.POST_DELETE, new CdmPostDataChangeObservableListener(), l3Listener);
66
//without validation
67
		eventRegistry.appendListeners(EventType.POST_INSERT, new CdmPostDataChangeObservableListener());
68
		eventRegistry.appendListeners(EventType.POST_UPDATE, new CdmPostDataChangeObservableListener());
69
		eventRegistry.appendListeners(EventType.POST_DELETE, new CdmPostDataChangeObservableListener());
70

  
71
		eventRegistry.appendListeners(EventType.PRE_INSERT, new CdmPreDataChangeObservableListener());
72
		eventRegistry.appendListeners(EventType.PRE_UPDATE, new CdmPreDataChangeObservableListener());
73
	}
74

  
75

  
76
	/*
77
	 * (non-Javadoc)
78
	 *
79
	 * @see org.hibernate.integrator.spi.Integrator#integrate(org.hibernate.metamodel.source.
80
	 * MetadataImplementor, org.hibernate.engine.spi.SessionFactoryImplementor,
81
	 * org.hibernate.service.spi.SessionFactoryServiceRegistry)
82
	 */
83
	@Override
84
	public void integrate(MetadataImplementor metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry){
85
		//nothing to do for now
86
		logger.warn("Metadata integrate not yet implemented");
87
	}
88

  
89

  
90
	/*
91
	 * (non-Javadoc)
92
	 *
93
	 * @see org.hibernate.integrator.spi.Integrator#disintegrate(org.hibernate.engine.spi.
94
	 * SessionFactoryImplementor, org.hibernate.service.spi.SessionFactoryServiceRegistry)
95
	 */
96
	@Override
97
	public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry)
98
	{
99
		//nothing to do for now
100
		logger.warn("Disintegrate not yet implemented");
101
	}
102

  
103
}
cdmlib-persistence/src/main/java/eu/etaxonomy/cdm/persistence/hibernate/CdmPreDataChangeObservableListener.java
9 9
*/
10 10
package eu.etaxonomy.cdm.persistence.hibernate;
11 11

  
12
import java.util.List;
13

  
12 14
import org.hibernate.event.spi.PreInsertEvent;
13 15
import org.hibernate.event.spi.PreInsertEventListener;
14 16
import org.hibernate.event.spi.PreUpdateEvent;
......
17 19
import org.springframework.security.core.Authentication;
18 20
import org.springframework.security.core.context.SecurityContextHolder;
19 21

  
22
import eu.etaxonomy.cdm.model.agent.Team;
23
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
24
import eu.etaxonomy.cdm.model.common.CdmBase;
20 25
import eu.etaxonomy.cdm.model.common.ICdmBase;
26
import eu.etaxonomy.cdm.model.common.ITreeNode;
27
import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
21 28
import eu.etaxonomy.cdm.model.common.User;
22 29
import eu.etaxonomy.cdm.model.common.VersionableEntity;
30
import eu.etaxonomy.cdm.model.molecular.Amplification;
31
import eu.etaxonomy.cdm.model.name.NonViralName;
32
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
33
import eu.etaxonomy.cdm.model.reference.Reference;
23 34

  
24 35
/**
25 36
 * @author cmathew
......
27 38
 *
28 39
 */
29 40
public class CdmPreDataChangeObservableListener implements PreInsertEventListener, PreUpdateEventListener {
41
    private static final long serialVersionUID = -7581071903134036209L;
42

  
43
    static String sep = ITreeNode.separator;
44
    static String pref = ITreeNode.treePrefix;
30 45

  
31
    /* (non-Javadoc)
32
     * @see org.hibernate.event.spi.PreUpdateEventListener#onPreUpdate(org.hibernate.event.spi.PreUpdateEvent)
33
     */
34 46
    @Override
35 47
    public boolean onPreUpdate(PreUpdateEvent event) {
36 48
        try {
......
44 56
                    versionableEntity.setUpdatedBy(user);
45 57
                }
46 58
            }
59
            insertUpdateMerge(event.getEntity());
47 60
        } finally {
48 61
            return false;
49 62
        }
50 63
    }
51 64

  
52
    /* (non-Javadoc)
53
     * @see org.hibernate.event.spi.PreInsertEventListener#onPreInsert(org.hibernate.event.spi.PreInsertEvent)
54
     */
55 65
    @Override
56 66
    public boolean onPreInsert(PreInsertEvent event) {
57 67
        try {
......
71 81
                    }
72 82
                }
73 83
            }
84
            insertUpdateMerge(entity);
74 85
        } finally {
75 86
            return false;
76 87
        }
77 88

  
78 89
    }
79 90

  
91
    //from former SaveOrUpdateOrMergeEntityListener
92
    public static void insertUpdateMerge(Object entity){
93
        if(entity != null && CdmBase.class.isAssignableFrom(entity.getClass())){
94
            generateTreeIndex(entity);
95
            cacheDeterminationNames(entity);
96
            generateCaches(entity);
97
        }
98
    }
99

  
100
    private static void cacheDeterminationNames(Object entity) {
101
        if (entity instanceof DeterminationEvent) {
102
            DeterminationEvent detEv = (DeterminationEvent)entity;
103
            if (detEv.getTaxon() != null && detEv.getTaxonName() == null && detEv.getTaxon().getName() != null){
104
                detEv.setTaxonName(detEv.getTaxon().getName());
105
            }
106
        }
107
    }
108

  
109
    private static void generateTreeIndex(Object entity) {
110
        if (entity instanceof ITreeNode) {
111
            ITreeNode<?> node = (ITreeNode<?>)entity;
112
            reindex(node);
113

  
114
        }
115
    }
116

  
117
    public static void generateCaches(Object entity){
118
        if (entity != null){
119
            Class<?> entityClazz = entity.getClass();
120

  
121
            //non-viral-name caches
122
            if(NonViralName.class.isAssignableFrom(entityClazz)) {
123
                NonViralName<?> nonViralName = (NonViralName<?>)entity;
124
                nonViralName.getAuthorshipCache();
125
                nonViralName.getNameCache();
126
                nonViralName.getTitleCache();
127
                nonViralName.getFullTitleCache();
128
                //team-or-person caches
129
            }else if(TeamOrPersonBase.class.isAssignableFrom(entityClazz)){
130
                TeamOrPersonBase<?> teamOrPerson = (TeamOrPersonBase<?>)entity;
131
                String nomTitle = teamOrPerson.getNomenclaturalTitle();
132
                if (teamOrPerson instanceof Team){
133
                    Team team =CdmBase.deproxy(teamOrPerson, Team.class);
134
                    team.setNomenclaturalTitle(nomTitle, team.isProtectedNomenclaturalTitleCache()); //nomTitle is not necessarily cached when it is created
135
                }else{
136
                    teamOrPerson.setNomenclaturalTitle(nomTitle);
137
                }
138
                String titleCache = teamOrPerson.getTitleCache();
139
                if (! teamOrPerson.isProtectedTitleCache()){
140
                    teamOrPerson.setTitleCache(titleCache, false);
141
                }
142

  
143
                //reference caches
144
            }else if(Reference.class.isAssignableFrom(entityClazz)){
145
                Reference<?> ref = (Reference<?>)entity;
146
                ref.getAbbrevTitleCache();
147
                ref.getTitleCache();
148
                //title cache
149
            }else if(IdentifiableEntity.class.isAssignableFrom(entityClazz)) {
150
                IdentifiableEntity<?> identifiableEntity = (IdentifiableEntity)entity;
151
                identifiableEntity.getTitleCache();
152
            }else if(Amplification.class.isAssignableFrom(entityClazz)) {
153
                Amplification amplification = (Amplification)entity;
154
                amplification.updateCache();
155
            }
156

  
157
        }
158
    }
159

  
160
    /**
161
     * @param event
162
     * @param node
163
     */
164
    private static <T extends ITreeNode> void reindex(T node) {
165
        String oldChildIndex = node.treeIndex();
166
        ITreeNode<?> parent = node.getParent();
167
        String parentIndex = (parent == null) ? (sep + pref + node.treeId() + sep)  : parent.treeIndex();  //TODO
168
        if (node.getId() > 0){   //TODO
169
            String newChildIndex = parentIndex + node.getId() + sep;
170
            if (oldChildIndex == null || ! oldChildIndex.equals(newChildIndex)){
171
                node.setTreeIndex(newChildIndex);
172

  
173
                //TODO this is a greedy implementation, better use update by replace string
174
                //either using and improving the below code or by using native SQL
175
                //The current approach may run out of memory for large descendant sets.
176
                List<T> childNodes = node.getChildNodes();
177
                for (T child : childNodes){
178
                    if (child != null && ! child.equals(node)){  //node should not be it's own child, however just in case
179
                        reindex(child);
180
                    }
181
                }
182

  
183
                //          String className = event.getEntityName();
184
                //                  String updateQuery = " UPDATE %s tn " +
185
                //                          " SET tn.treeIndex = Replace(tn.treeIndex, '%s', '%s') " +
186
                //                          " WHERE tn.id <> "+ node.getId()+" ";
187
                //                  updateQuery = String.format(updateQuery, className, oldChildIndex, parentIndex);  //dummy
188
                //                  System.out.println(updateQuery);
189
                //                  EventSource session = event.getSession();
190
                //                  Query query = session.createQuery(updateQuery);
191
                //                  query.executeUpdate();
192
            }
193
        }
194
    }
195

  
80 196
}
cdmlib-persistence/src/main/java/eu/etaxonomy/cdm/persistence/hibernate/CdmSecurityHibernateInterceptor.java
1
/**
2
 * Copyright (C) 2011 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.persistence.hibernate;
10

  
11

  
12

  
13
import java.io.Serializable;
14
import java.util.EnumSet;
15
import java.util.HashMap;
16
import java.util.HashSet;
17
import java.util.Map;
18
import java.util.Set;
19

  
20
import org.apache.commons.lang.ArrayUtils;
21
import org.apache.log4j.Logger;
22
import org.hibernate.EmptyInterceptor;
23
import org.hibernate.type.Type;
24
import org.springframework.security.core.context.SecurityContextHolder;
25
import org.springframework.stereotype.Component;
26

  
27
import eu.etaxonomy.cdm.database.PermissionDeniedException;
28
import eu.etaxonomy.cdm.model.CdmBaseType;
29
import eu.etaxonomy.cdm.model.common.CdmBase;
30
import eu.etaxonomy.cdm.model.common.IPublishable;
31
import eu.etaxonomy.cdm.persistence.hibernate.permission.CRUD;
32
import eu.etaxonomy.cdm.persistence.hibernate.permission.CdmPermissionEvaluator;
33
import eu.etaxonomy.cdm.persistence.hibernate.permission.Operation;
34
import eu.etaxonomy.cdm.persistence.hibernate.permission.Role;
35
/**
36
 * @author k.luther
37
 * @author a.kohlbecker
38
 *
39
 */
40
@Component
41
public class CdmSecurityHibernateInterceptor extends EmptyInterceptor {
42

  
43
    private static final long serialVersionUID = 8477758472369568074L;
44

  
45
    public static final Logger logger = Logger.getLogger(CdmSecurityHibernateInterceptor.class);
46

  
47

  
48
    private CdmPermissionEvaluator permissionEvaluator;
49

  
50
    public CdmPermissionEvaluator getPermissionEvaluator() {
51
        return permissionEvaluator;
52
    }
53

  
54
    public void setPermissionEvaluator(CdmPermissionEvaluator permissionEvaluator) {
55
        this.permissionEvaluator = permissionEvaluator;
56
    }
57

  
58
    /**
59
     * The exculdeMap must map every property to the CdmBase type !!!
60
     */
61
    public static final Map<Class<? extends CdmBase>, Set<String>> exculdeMap = new HashMap<Class<? extends CdmBase>, Set<String>>();
62

  
63
    static{
64
//        disabled since no longer needed, see https://dev.e-taxonomy.eu/trac/ticket/4111#comment:8
65
//        exculdeMap.put(TaxonNameBase.class, new HashSet<String>());
66

  
67
        /*
68
         * default fields required for each type for which excludes are defined
69
         */
70
//        exculdeMap.get(TaxonNameBase.class).add("updatedBy");
71
//        exculdeMap.get(TaxonNameBase.class).add("created");
72
//        exculdeMap.get(TaxonNameBase.class).add("updated");
73

  
74
        /*
75
         * the specific excludes
76
         */
77
//        exculdeMap.get(TaxonNameBase.class).add("taxonBases");
78
    }
79

  
80

  
81
    /* (non-Javadoc)
82
     * @see org.hibernate.EmptyInterceptor#onSave(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
83
     */
84
    @Override
85
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] type) {
86

  
87
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
88
            return true;
89
        }
90
        // evaluate throws EvaluationFailedException
91
        checkPermissions((CdmBase) entity, Operation.CREATE);
92
        logger.debug("permission check suceeded - object creation granted");
93
        return true;
94
    }
95

  
96

  
97
    /* (non-Javadoc)
98
     * @see org.hibernate.EmptyInterceptor#onFlushDirty(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
99
     */
100
    @Override
101
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
102

  
103
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
104
            return true;
105
        }
106
        CdmBase cdmEntity = (CdmBase) entity;
107
        if (previousState == null){
108
            return onSave(cdmEntity, id, currentState, propertyNames, null);
109
        }
110
        if (isModified(currentState, previousState, propertyNames, exculdeMap.get(CdmBaseType.baseTypeFor(cdmEntity.getClass())))) {
111
            // evaluate throws EvaluationFailedException
112
            checkPermissions(cdmEntity, Operation.UPDATE);
113
            logger.debug("Operation.UPDATE permission check suceeded - object update granted");
114

  
115
            if(IPublishable.class.isAssignableFrom(entity.getClass())){
116
                if(namedPropertyIsModified(currentState, previousState, propertyNames, "publish")){
117
                    checkRoles(Role.ROLE_PUBLISH, Role.ROLE_ADMIN);
118
                    logger.debug("Role.ROLE_PUBLISH permission check suceeded - object update granted");
119
                }
120
            }
121
        }
122
        return true;
123
    }
124

  
125

  
126

  
127
    /* (non-Javadoc)
128
     * @see org.hibernate.EmptyInterceptor#onDelete(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
129
     */
130
    @Override
131
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
132

  
133
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
134
            return;
135
        }
136
        CdmBase cdmEntity = (CdmBase) entity;
137
        // evaluate throws EvaluationFailedException
138
        checkPermissions(cdmEntity, Operation.DELETE);
139
        logger.debug("permission check suceeded - object update granted");
140
        return;
141
    }
142

  
143
    /**
144
     * checks if the current authentication has the <code>expectedPermission</code> on the supplied <code>entity</code>.
145
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
146
     *
147
     * @param entity
148
     * @param expectedOperation
149
     */
150
    private void checkPermissions(CdmBase entity, EnumSet<CRUD> expectedOperation) {
151

  
152
        if (!permissionEvaluator.hasPermission(SecurityContextHolder.getContext().getAuthentication(), entity, expectedOperation)){
153
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), entity, expectedOperation);
154
        }
155
    }
156

  
157
    /**
158
     * checks if the current authentication has at least one of the <code>roles</code>.
159
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
160
     * @param roles
161
     */
162
    private void checkRoles(Role ... roles) {
163

  
164
        if (!permissionEvaluator.hasOneOfRoles(SecurityContextHolder.getContext().getAuthentication(), roles)){
165
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), roles);
166
        }
167
    }
168

  
169
    /**
170
     * Checks if the CDM entity as been modified by comparing the current with the previous state.
171
     *
172
     * @param currentState
173
     * @param previousState
174
     * @return true if the currentState and previousState differ.
175
     */
176
    private boolean isModified(Object[] currentState, Object[] previousState, String[] propertyNames, Set<String> excludes) {
177

  
178
        Set<Integer> excludeIds = null;
179

  
180
        if(excludes != null && excludes.size() > 0) {
181
            excludeIds = new HashSet<Integer>(excludes.size());
182
            int i = 0;
183
            for(String prop : propertyNames){
184
                if(excludes.contains(prop)){
185
                    excludeIds.add(i);
186
                }
187
                if(excludeIds.size() == excludes.size()){
188
                    // all ids found
189
                    break;
190
                }
191
                i++;
192
            }
193
        }
194

  
195
        for (int i = 0; i<currentState.length; i++){
196
            if((excludeIds == null || !excludeIds.contains(i))){
197
                if(propertyIsModified(currentState, previousState, i)){
198
                    if(logger.isDebugEnabled()){
199
                        logger.debug("modified property found: " + propertyNames[i] + ", previousState: " + previousState[i] + ", currentState: " + currentState[i] );
200
                    }
201
                    return true;
202
                }
203
            }
204
        }
205

  
206
        return false;
207
    }
208

  
209
    /**
210
     * Compares the object states at the property denoted by the key parameter and returns true if they differ in this property
211
     *
212
     * @param currentState
213
     * @param previousState
214
     * @param isModified
215
     * @param key
216
     * @return
217
     */
218
    private boolean propertyIsModified(Object[] currentState, Object[] previousState, int key) {
219
        if (currentState[key]== null ) {
220
            if ( previousState[key]!= null) {
221
                return true;
222
            }
223
        }
224
        if (currentState[key]!= null ){
225
            if (previousState[key] == null){
226
                return true;
227
            }
228
        }
229
        if (currentState[key]!= null && previousState[key] != null){
230
            if (!currentState[key].equals(previousState[key])) {
231
                return true;
232
            }
233
        }
234
        return false;
235
    }
236

  
237
    private boolean namedPropertyIsModified(Object[] currentState, Object[] previousState, String[] propertyNames, String propertyNameToTest) {
238

  
239
        int key = ArrayUtils.indexOf(propertyNames, propertyNameToTest);
240
        return propertyIsModified(currentState, previousState, key);
241
    }
242

  
243

  
244
}
1
/**
2
 * Copyright (C) 2011 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.persistence.hibernate;
10

  
11

  
12

  
13
import java.io.Serializable;
14
import java.util.EnumSet;
15
import java.util.HashMap;
16
import java.util.HashSet;
17
import java.util.Map;
18
import java.util.Set;
19

  
20
import org.apache.commons.lang.ArrayUtils;
21
import org.apache.log4j.Logger;
22
import org.hibernate.EmptyInterceptor;
23
import org.hibernate.type.Type;
24
import org.springframework.security.core.context.SecurityContextHolder;
25
import org.springframework.stereotype.Component;
26

  
27
import eu.etaxonomy.cdm.database.PermissionDeniedException;
28
import eu.etaxonomy.cdm.model.CdmBaseType;
29
import eu.etaxonomy.cdm.model.common.CdmBase;
30
import eu.etaxonomy.cdm.model.common.IPublishable;
31
import eu.etaxonomy.cdm.persistence.hibernate.permission.CRUD;
32
import eu.etaxonomy.cdm.persistence.hibernate.permission.CdmPermissionEvaluator;
33
import eu.etaxonomy.cdm.persistence.hibernate.permission.Operation;
34
import eu.etaxonomy.cdm.persistence.hibernate.permission.Role;
35
/**
36
 * @author k.luther
37
 * @author a.kohlbecker
38
 *
39
 */
40
@Component
41
public class CdmSecurityHibernateInterceptor extends EmptyInterceptor {
42

  
43
    private static final long serialVersionUID = 8477758472369568074L;
44

  
45
    public static final Logger logger = Logger.getLogger(CdmSecurityHibernateInterceptor.class);
46

  
47

  
48
    private CdmPermissionEvaluator permissionEvaluator;
49

  
50
    public CdmPermissionEvaluator getPermissionEvaluator() {
51
        return permissionEvaluator;
52
    }
53

  
54
    public void setPermissionEvaluator(CdmPermissionEvaluator permissionEvaluator) {
55
        this.permissionEvaluator = permissionEvaluator;
56
    }
57

  
58
    /**
59
     * The exculdeMap must map every property to the CdmBase type !!!
60
     */
61
    public static final Map<Class<? extends CdmBase>, Set<String>> exculdeMap = new HashMap<Class<? extends CdmBase>, Set<String>>();
62

  
63
    static{
64
//        disabled since no longer needed, see https://dev.e-taxonomy.eu/trac/ticket/4111#comment:8
65
//        exculdeMap.put(TaxonNameBase.class, new HashSet<String>());
66

  
67
        Set<String> defaultExculdes = new HashSet<String>();
68
        defaultExculdes.add("createdBy");
69

  
70
        for ( CdmBaseType type: CdmBaseType.values()){
71
            exculdeMap.put(type.getBaseClass(), new HashSet<String>());
72
            exculdeMap.get(type.getBaseClass()).addAll(defaultExculdes);
73
        }
74
        exculdeMap.put(CdmBase.class, new HashSet<String>());
75
        exculdeMap.get(CdmBase.class).addAll(defaultExculdes);
76

  
77

  
78
        /*
79
         * default fields required for each type for which excludes are defined
80
         */
81
//        exculdeMap.get(TaxonNameBase.class).add("updatedBy");
82
//        exculdeMap.get(TaxonNameBase.class).add("created");
83
//        exculdeMap.get(TaxonNameBase.class).add("updated");
84

  
85
        /*
86
         * the specific excludes
87
         */
88
//        exculdeMap.get(TaxonNameBase.class).add("taxonBases");
89
    }
90

  
91

  
92
    /* (non-Javadoc)
93
     * @see org.hibernate.EmptyInterceptor#onSave(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
94
     */
95
    @Override
96
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] type) {
97

  
98
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
99
            return true;
100
        }
101
        // evaluate throws EvaluationFailedException
102
        checkPermissions((CdmBase) entity, Operation.CREATE);
103
        logger.debug("permission check suceeded - object creation granted");
104
        return true;
105
    }
106

  
107

  
108
    /* (non-Javadoc)
109
     * @see org.hibernate.EmptyInterceptor#onFlushDirty(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
110
     */
111
    @Override
112
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
113

  
114
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
115
            return true;
116
        }
117
        CdmBase cdmEntity = (CdmBase) entity;
118
        if (previousState == null){
119
            return onSave(cdmEntity, id, currentState, propertyNames, null);
120
        }
121
        if (isModified(currentState, previousState, propertyNames, exculdeMap.get(baseType(cdmEntity)))) {
122
            // evaluate throws EvaluationFailedException
123
            checkPermissions(cdmEntity, Operation.UPDATE);
124
            logger.debug("Operation.UPDATE permission check suceeded - object update granted");
125

  
126
            if(IPublishable.class.isAssignableFrom(entity.getClass())){
127
                if(namedPropertyIsModified(currentState, previousState, propertyNames, "publish")){
128
                    checkRoles(Role.ROLE_PUBLISH, Role.ROLE_ADMIN);
129
                    logger.debug("Role.ROLE_PUBLISH permission check suceeded - object update granted");
130
                }
131
            }
132
        }
133
        return true;
134
    }
135

  
136
    private Class<? extends CdmBase> baseType(CdmBase cdmEntity) {
137
        Class<? extends CdmBase> basetype = CdmBaseType.baseTypeFor(cdmEntity.getClass());
138
        return basetype == null ? CdmBase.class : basetype;
139
    }
140

  
141

  
142

  
143
    /* (non-Javadoc)
144
     * @see org.hibernate.EmptyInterceptor#onDelete(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
145
     */
146
    @Override
147
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
148

  
149
        if (SecurityContextHolder.getContext().getAuthentication() == null || !(entity instanceof CdmBase)) {
150
            return;
151
        }
152
        CdmBase cdmEntity = (CdmBase) entity;
153
        // evaluate throws EvaluationFailedException
154
        checkPermissions(cdmEntity, Operation.DELETE);
155
        logger.debug("permission check suceeded - object update granted");
156
        return;
157
    }
158

  
159
    /**
160
     * checks if the current authentication has the <code>expectedPermission</code> on the supplied <code>entity</code>.
161
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
162
     *
163
     * @param entity
164
     * @param expectedOperation
165
     */
166
    private void checkPermissions(CdmBase entity, EnumSet<CRUD> expectedOperation) {
167

  
168
        if (!permissionEvaluator.hasPermission(SecurityContextHolder.getContext().getAuthentication(), entity, expectedOperation)){
169
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), entity, expectedOperation);
170
        }
171
    }
172

  
173
    /**
174
     * checks if the current authentication has at least one of the <code>roles</code>.
175
     * Throws an {@link PermissionDeniedException} if the evaluation fails.
176
     * @param roles
177
     */
178
    private void checkRoles(Role ... roles) {
179

  
180
        if (!permissionEvaluator.hasOneOfRoles(SecurityContextHolder.getContext().getAuthentication(), roles)){
181
            throw new PermissionDeniedException(SecurityContextHolder.getContext().getAuthentication(), roles);
182
        }
183
    }
184

  
185
    /**
186
     * Checks if the CDM entity as been modified by comparing the current with the previous state.
187
     *
188
     * @param currentState
189
     * @param previousState
190
     * @return true if the currentState and previousState differ.
191
     */
192
    private boolean isModified(Object[] currentState, Object[] previousState, String[] propertyNames, Set<String> excludes) {
193

  
194
        Set<Integer> excludeIds = null;
195

  
196
        if(excludes != null && excludes.size() > 0) {
197
            excludeIds = new HashSet<Integer>(excludes.size());
198
            int i = 0;
199
            for(String prop : propertyNames){
200
                if(excludes.contains(prop)){
201
                    excludeIds.add(i);
202
                }
203
                if(excludeIds.size() == excludes.size()){
204
                    // all ids found
205
                    break;
206
                }
207
                i++;
208
            }
209
        }
210

  
211
        for (int i = 0; i<currentState.length; i++){
212
            if((excludeIds == null || !excludeIds.contains(i))){
213
                if(propertyIsModified(currentState, previousState, i)){
214
                    if(logger.isDebugEnabled()){
215
                        logger.debug("modified property found: " + propertyNames[i] + ", previousState: " + previousState[i] + ", currentState: " + currentState[i] );
216
                    }
217
                    return true;
218
                }
219
            }
220
        }
221

  
222
        return false;
223
    }
224

  
225
    /**
226
     * Compares the object states at the property denoted by the key parameter and returns true if they differ in this property
227
     *
228
     * @param currentState
229
     * @param previousState
230
     * @param isModified
231
     * @param key
232
     * @return
233
     */
234
    private boolean propertyIsModified(Object[] currentState, Object[] previousState, int key) {
235
        if (currentState[key]== null ) {
236
            if ( previousState[key]!= null) {
237
                return true;
238
            }
239
        }
240
        if (currentState[key]!= null ){
241
            if (previousState[key] == null){
242
                return true;
243
            }
244
        }
245
        if (currentState[key]!= null && previousState[key] != null){
246
            if (!currentState[key].equals(previousState[key])) {
247
                return true;
248
            }
249
        }
250
        return false;
251
    }
252

  
253
    private boolean namedPropertyIsModified(Object[] currentState, Object[] previousState, String[] propertyNames, String propertyNameToTest) {
254

  
255
        int key = ArrayUtils.indexOf(propertyNames, propertyNameToTest);
256
        return propertyIsModified(currentState, previousState, key);
257
    }
258

  
259

  
260
}
cdmlib-persistence/src/main/java/eu/etaxonomy/cdm/persistence/hibernate/SaveOrUpdateorMergeEntityListener.java
19 19
import org.hibernate.event.spi.SaveOrUpdateEvent;
20 20
import org.hibernate.event.spi.SaveOrUpdateEventListener;
21 21

  
22
import eu.etaxonomy.cdm.model.common.CdmBase;
23 22
import eu.etaxonomy.cdm.model.common.ITreeNode;
24
import eu.etaxonomy.cdm.model.occurrence.DeterminationEvent;
25 23

  
26 24

  
27 25
@SuppressWarnings("serial")
......
34 32
        saveOrUpdateOrMerge(entity, event.getSession());
35 33
    }
36 34

  
37
    /* (non-Javadoc)
38
     * @see org.hibernate.event.spi.MergeEventListener#onMerge(org.hibernate.event.spi.MergeEvent)
39
     */
40 35
    @Override
41 36
    public void onMerge(MergeEvent event) throws HibernateException {
42 37
        Object entity = event.getOriginal();
43 38
        saveOrUpdateOrMerge(entity,event.getSession());
44 39
    }
45 40

  
46
    /* (non-Javadoc)
47
     * @see org.hibernate.event.spi.MergeEventListener#onMerge(org.hibernate.event.spi.MergeEvent, java.util.Map)
48
     */
49 41
    @Override
50 42
    public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException {
51 43

  
......
53 45

  
54 46
    private void saveOrUpdateOrMerge(Object entity, Session session) {
55 47

  
56
        if(entity != null && CdmBase.class.isAssignableFrom(entity.getClass())){
57

  
58
            if (entity instanceof ITreeNode) {
59
                ITreeNode<?> node = (ITreeNode<?>)entity;
60
                reindex(node);
61

  
62
            }
63

  
64
            if (entity instanceof DeterminationEvent) {
65
                DeterminationEvent detEv = (DeterminationEvent)entity;
66
                if (detEv.getTaxon() != null && detEv.getTaxonName() == null && detEv.getTaxon().getName() != null){
67
                    detEv.setTaxonName(detEv.getTaxon().getName());
68
                }
69
            }
70
        }
48
        //moved to CdmPreDataChangeObservableListener
49
//        if(entity != null && CdmBase.class.isAssignableFrom(entity.getClass())){
50
//
51
//            if (entity instanceof ITreeNode) {
52
//                ITreeNode<?> node = (ITreeNode<?>)entity;
53
//                reindex(node);
54
//
55
//            }
56
//
57
//            if (entity instanceof DeterminationEvent) {
58
//                DeterminationEvent detEv = (DeterminationEvent)entity;
59
//                if (detEv.getTaxon() != null && detEv.getTaxonName() == null && detEv.getTaxon().getName() != null){
60
//                    detEv.setTaxonName(detEv.getTaxon().getName());
61
//                }
62
//            }
63
//        }
71 64
    }
72 65

  
73 66
    static String sep = ITreeNode.separator;
cdmlib-persistence/src/test/resources/eu/etaxonomy/cdm/persistence/dao/hibernate/description/DescriptionElementDaoHibernateImplTest.testChangeLanguageString-result.xml
1
<?xml version='1.0' encoding='UTF-8'?>

1
<?xml version='1.0' encoding='UTF-8'?>
2 2
<dataset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../dataset.xsd">
3
  <DESCRIPTIONELEMENTBASE DTYPE="TextData" ID="34" INDESCRIPTION_ID="1" CREATED="2008-12-10 09:56:07.0" UUID="31a0160a-51b2-4565-85cf-2be58cb561d6" FEATURE_ID="922"/>

4
  <DESCRIPTIONELEMENTBASE_LANGUAGESTRING DESCRIPTIONELEMENTBASE_ID="34" MULTILANGUAGETEXT_MAPKEY_ID="406"/>

5
  <LANGUAGESTRING ID="1" CREATED="2008-12-10 09:56:07.0" UUID="2a5ceebb-4830-4524-b330-78461bf8cb6b" UPDATED="2008-12-10 09:56:07.253" LANGUAGE_ID="406" TEXT="A new English text"/>

6
  <LANGUAGESTRING ID="2" CREATED="2008-12-10 09:56:07.0" UUID="373e7154-9372-4985-b77e-68df28e3f84b" UPDATED="2008-12-10 09:56:07.253" LANGUAGE_ID="406" TEXT="Praesent vitae turpis vitae sapien sodales sagittis."/>

7
  <LANGUAGESTRING ID="3" CREATED="2008-12-10 09:56:07.0" UUID="f72f17d8-58c2-4c4e-b052-89d9016b6d02" UPDATED="2008-12-10 09:56:07.253" LANGUAGE_ID="406" TEXT="Maecenas congue ligula ut nulla. Nullam commodo euismod dolor."/>

8
  
3
  <DESCRIPTIONELEMENTBASE DTYPE="TextData" ID="34" INDESCRIPTION_ID="1" CREATED="2008-12-10 09:56:07.0" UUID="31a0160a-51b2-4565-85cf-2be58cb561d6" FEATURE_ID="922"/>
4
  <DESCRIPTIONELEMENTBASE_LANGUAGESTRING DESCRIPTIONELEMENTBASE_ID="34" MULTILANGUAGETEXT_MAPKEY_ID="406"/>
5
  <LANGUAGESTRING ID="1" CREATED="2008-12-10 09:56:07.0" UUID="2a5ceebb-4830-4524-b330-78461bf8cb6b" LANGUAGE_ID="406" TEXT="A new English text"/>
6
  <LANGUAGESTRING ID="2" CREATED="2008-12-10 09:56:07.0" UUID="373e7154-9372-4985-b77e-68df28e3f84b" UPDATED="2008-12-10 09:56:07.253" LANGUAGE_ID="406" TEXT="Praesent vitae turpis vitae sapien sodales sagittis."/>
7
  <LANGUAGESTRING ID="3" CREATED="2008-12-10 09:56:07.0" UUID="f72f17d8-58c2-4c4e-b052-89d9016b6d02" UPDATED="2008-12-10 09:56:07.253" LANGUAGE_ID="406" TEXT="Maecenas congue ligula ut nulla. Nullam commodo euismod dolor."/>
8
  
9 9
</dataset>
cdmlib-persistence/src/test/resources/eu/etaxonomy/cdm/persistence/dao/hibernate/taxon/TaxonDaoHibernateImplTest.testAddChild-result.xml
125 125
  <TAXONBASE DTYPE="Taxon" ID="33" CREATED="2007-12-10 09:56:07.0" UUID="2c41e444-b160-4c6a-a1be-d5317d97d68d" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="Manduca bergarmatipes (Clark, 1927) sec. cate-sphingidae.org" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="0" NAME_ID="33" SEC_ID="2"/>
126 126
  <TAXONBASE DTYPE="Taxon" ID="34" CREATED="2009-12-10 09:56:07.0" UUID="7fe66bfd-235b-4164-8f0a-d054b5e962ba" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="Manduca chinchilla (Gehlen, 1942) sec. cate-sphingidae.org" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="0" NAME_ID="34" SEC_ID="2"/>
127 127
  <TAXONBASE DTYPE="Taxon" ID="35" CREATED="2002-12-10 09:56:07.0" UUID="4cab3cc5-eb80-477c-ac1b-be3c3d0a5a85" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="Acherontia atropos (Linnaeus, 1758) sec. cate-sphingidae.org" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="0" NAME_ID="35" SEC_ID="2"/>
128
  <TAXONBASE DTYPE="Taxon" ID="36" CREATED="2003-04-10 09:56:07.0" UUID="b04cc9cb-2b4a-4cc4-a94a-3c93a2158b06" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="Acherontia lachesis (Fabricius, 1798) sec. cate-sphingidae.org" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="1" NAME_ID="36" SEC_ID="2"/>
128
  <TAXONBASE DTYPE="Taxon" ID="36" CREATED="2003-04-10 09:56:07.0" UUID="b04cc9cb-2b4a-4cc4-a94a-3c93a2158b06" PROTECTEDTITLECACHE="true" TITLECACHE="Acherontia lachesis (Fabricius, 1798) sec. cate-sphingidae.org" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="1" NAME_ID="36" SEC_ID="2"/>
129 129
  <TAXONBASE DTYPE="Taxon" ID="37" CREATED="2003-08-10 09:56:07.0" UUID="7b8b5cb3-37ba-4dba-91ac-4c6ffd6ac331" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="Acherontia styx Westwood, 1847 sec. cate-sphingidae.org" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="1" NAME_ID="37" SEC_ID="2"/>
130 130
  <TAXONBASE DTYPE="Taxon" ID="38" CREATED="2008-12-10 09:56:07.0" UUID="bc09aca6-06fd-4905-b1e7-cbf7cc65d783" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="Cryptocoryne x purpurea nothovar borneoensis N.Jacobsen, Bastm. &amp; Yuji Sasaki sec. cate-sphingidae.org" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="0" NAME_ID="38" SEC_ID="3"/>
131 131
  <TAXONBASE DTYPE="Taxon" PROTECTEDTITLECACHE="true" TITLECACHE="Acherontia lachesis diehli Eitschberger, 2003" DOUBTFUL="false" TAXONOMICCHILDRENCOUNT="0" TAXONOMICPARENTCACHE_ID="36"/>
cdmlib-persistence/src/test/resources/eu/etaxonomy/cdm/persistence/hibernate/replace/ReferringObjectMetadataFactoryTest.testReplaceToOneProperty-result.xml
1 1
<?xml version='1.0' encoding='UTF-8'?>
2 2
<dataset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dao/hibernate/dataset.xsd">
3
  <AGENTBASE DTYPE="Person" ID="1" CREATED="2008-12-10 09:56:07.0" UUID="e4ec436a-3e8c-4166-a834-3bb84c2b5ad6" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="H.C.J. Godfray"  COLLECTORTITLE="[NULL]" PROTECTEDCOLLECTORTITLECACHE="FALSE" />
4
  <AGENTBASE DTYPE="Person" ID="2" CREATED="2008-12-10 09:56:07.0" UUID="ed6ac546-8c6c-48c4-9b91-40b1157c05c6" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="B.R. Clark" FIRSTNAME="Ben" LASTNAME="Clark"  COLLECTORTITLE="[NULL]" PROTECTEDCOLLECTORTITLECACHE="FALSE" />
3
  <AGENTBASE DTYPE="Person" ID="1" CREATED="2008-12-10 09:56:07.0" UUID="e4ec436a-3e8c-4166-a834-3bb84c2b5ad6" PROTECTEDTITLECACHE="true" TITLECACHE="H.C.J. Godfray"  COLLECTORTITLE="[NULL]" PROTECTEDCOLLECTORTITLECACHE="FALSE" />
4
  <AGENTBASE DTYPE="Person" ID="2" CREATED="2008-12-10 09:56:07.0" UUID="ed6ac546-8c6c-48c4-9b91-40b1157c05c6" PROTECTEDTITLECACHE="true" TITLECACHE="B.R. Clark" FIRSTNAME="Ben" LASTNAME="Clark"  COLLECTORTITLE="[NULL]" PROTECTEDCOLLECTORTITLECACHE="FALSE" />
5 5
  <AGENTBASE DTYPE="Person" ID="3" CREATED="2008-12-10 09:56:07.0" UUID="746e872b-3f61-442c-b093-6b4d15c87694" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="I.J. Kitching"  COLLECTORTITLE="[NULL]" PROTECTEDCOLLECTORTITLECACHE="FALSE" />
6 6
  <AGENTBASE DTYPE="Person" ID="4" CREATED="2008-12-10 09:56:07.0" UUID="c62cd389-d787-47f4-99c3-b80eb12a1ef2" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="S.J. Mayo"  COLLECTORTITLE="[NULL]" PROTECTEDCOLLECTORTITLECACHE="FALSE" />
7 7
  <AGENTBASE DTYPE="Person" ID="5" CREATED="2008-12-10 09:56:07.0" UUID="dbaa601e-806b-40aa-a3cd-c2e179ddbd9a" UPDATED="2008-12-10 09:56:07.253" PROTECTEDTITLECACHE="true" TITLECACHE="M.J. Scoble"  COLLECTORTITLE="[NULL]" PROTECTEDCOLLECTORTITLECACHE="FALSE" />
cdmlib-services/src/test/java/eu/etaxonomy/cdm/api/service/SecurityTest.java
1
/**
2
 * Copyright (C) 2011 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.api.service;
10

  
11
import static org.junit.Assert.assertEquals;
12
import static org.junit.Assert.assertFalse;
13
import static org.junit.Assert.assertTrue;
14

  
15
import java.io.FileNotFoundException;
16
import java.util.Collection;
17
import java.util.EnumSet;
18
import java.util.HashSet;
19
import java.util.List;
20
import java.util.Set;
21
import java.util.UUID;
22

  
23
import javax.sql.DataSource;
24

  
25
import org.apache.log4j.Logger;
26
import org.junit.Assert;
27
import org.junit.Ignore;
28
import org.junit.Test;
29
import org.springframework.security.access.AccessDeniedException;
30
import org.springframework.security.authentication.AuthenticationManager;
31
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
32
import org.springframework.security.authentication.dao.SaltSource;
33
import org.springframework.security.authentication.encoding.PasswordEncoder;
34
import org.springframework.security.core.Authentication;
35
import org.springframework.security.core.GrantedAuthority;
36
import org.springframework.security.core.context.SecurityContext;
37
import org.springframework.security.core.context.SecurityContextHolder;
38
import org.unitils.database.annotations.TestDataSource;
39
import org.unitils.dbunit.annotation.DataSet;
40
import org.unitils.spring.annotation.SpringBean;
41
import org.unitils.spring.annotation.SpringBeanByType;
42

  
43
import sun.security.provider.PolicyParser.ParsingException;
44
import eu.etaxonomy.cdm.database.PermissionDeniedException;
45
import eu.etaxonomy.cdm.model.common.GrantedAuthorityImpl;
46
import eu.etaxonomy.cdm.model.common.User;
47
import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
48
import eu.etaxonomy.cdm.model.description.Feature;
49
import eu.etaxonomy.cdm.model.description.TaxonDescription;
50
import eu.etaxonomy.cdm.model.description.TextData;
51
import eu.etaxonomy.cdm.model.name.BotanicalName;
52
import eu.etaxonomy.cdm.model.name.Rank;
53
import eu.etaxonomy.cdm.model.name.TaxonNameBase;
54
import eu.etaxonomy.cdm.model.name.ZoologicalName;
55
import eu.etaxonomy.cdm.model.reference.Reference;
56
import eu.etaxonomy.cdm.model.reference.ReferenceFactory;
57
import eu.etaxonomy.cdm.model.taxon.Classification;
58
import eu.etaxonomy.cdm.model.taxon.Synonym;
59
import eu.etaxonomy.cdm.model.taxon.SynonymRelationshipType;
60
import eu.etaxonomy.cdm.model.taxon.Taxon;
61
import eu.etaxonomy.cdm.model.taxon.TaxonBase;
62
import eu.etaxonomy.cdm.model.taxon.TaxonNode;
63
import eu.etaxonomy.cdm.persistence.hibernate.permission.CRUD;
64
import eu.etaxonomy.cdm.persistence.hibernate.permission.CdmAuthority;
65
import eu.etaxonomy.cdm.persistence.hibernate.permission.CdmPermissionClass;
66
import eu.etaxonomy.cdm.persistence.hibernate.permission.CdmPermissionEvaluator;
67
import eu.etaxonomy.cdm.persistence.hibernate.permission.Operation;
68
import eu.etaxonomy.cdm.persistence.query.MatchMode;
69

  
70

  
71
@DataSet
72
public class SecurityTest extends AbstractSecurityTestBase{
73

  
74

  
75
    private static final Logger logger = Logger.getLogger(SecurityTest.class);
76

  
77
    @SpringBeanByType
78
    private ITaxonService taxonService;
79

  
80
    @SpringBeanByType
81
    private INameService nameService;
82

  
83
    @SpringBeanByType
84
    private IReferenceService referenceService;
85

  
86
    @SpringBeanByType
87
    private ITaxonNodeService taxonNodeService;
88

  
89
    @SpringBeanByType
90
    private IDescriptionService descriptionService;
91

  
92
    @SpringBeanByType
93
    private IUserService userService;
94

  
95
    @SpringBeanByType
96
    private IClassificationService classificationService;
97

  
98
    @SpringBeanByType
99
    private AuthenticationManager authenticationManager;
100

  
101
    @SpringBeanByType
102
    private SaltSource saltSource;
103

  
104
    @SpringBeanByType
105
    private PasswordEncoder passwordEncoder;
106

  
107
    @SpringBean("cdmPermissionEvaluator")
108
    private CdmPermissionEvaluator permissionEvaluator;
109

  
110
    @TestDataSource
111
    protected DataSource dataSource;
112

  
113
    private Authentication authentication;
114

  
115

  
116
    /**
117
     * no assertions in this test, since it is only used to create password hashes for test data
118
     */
119
    @Test
120
    public void testEncryptPassword(){
121

  
122
        String password = PASSWORD_ADMIN;
123
        User user = User.NewInstance("userManager", "");
124

  
125
        Object salt = this.saltSource.getSalt(user);
126
        String passwordEncrypted = passwordEncoder.encodePassword(password, salt);
127
        logger.info("encrypted password: " + passwordEncrypted );
128
    }
129

  
130
    @Test
131
    @DataSet
132
    public void testHasPermission(){
133

  
134
        Taxon taxon = Taxon.NewInstance(BotanicalName.NewInstance(Rank.GENUS()),null);
135

  
136
        authentication = authenticationManager.authenticate(tokenForTaxonomist);
137
        boolean hasPermission = permissionEvaluator.hasPermission(authentication, taxon, Operation.UPDATE);
138
        assertTrue(hasPermission);
139

  
140
        authentication = authenticationManager.authenticate(tokenForDescriptionEditor);
141
        hasPermission = permissionEvaluator.hasPermission(authentication, taxon, Operation.UPDATE);
142
        assertFalse(hasPermission);
143
    }
144

  
145
    @Test
146
    @DataSet
147
    public void testListByUsernameAllow(){
148

  
149
        authentication = authenticationManager.authenticate(tokenForTaxonomist);
150
        SecurityContext context = SecurityContextHolder.getContext();
151
        context.setAuthentication(authentication);
152

  
153
        List<User> userList = userService.listByUsername("Editor", MatchMode.ANYWHERE, null, null, 0, null, null);
154
        Assert.assertTrue("The user list must have elements", userList.size() > 0 );
155
    }
156

  
157
    @Test
158
    @DataSet
159
    public void testUserService_CreateDeny(){
160

  
161
        authentication = authenticationManager.authenticate(tokenForTaxonomist);
162
        SecurityContext context = SecurityContextHolder.getContext();
163
        context.setAuthentication(authentication);
164

  
165
        RuntimeException exception = null;
166
        try {
167
            userService.createUser(User.NewInstance("new guy", "alkjdsfalkj"));
168
            commitAndStartNewTransaction(null);
169
        } catch (AccessDeniedException e){
170
            logger.debug("Expected failure of evaluation.", e);
171
            exception = e;
172
        } catch (RuntimeException e){
173
            exception = findThrowableOfTypeIn(PermissionDeniedException.class, e);
174
            logger.debug("Expected failure of evaluation.", e);
175
        } finally {
176
            // needed in case saveOrUpdate was interrupted by the RuntimeException
177
            // commitAndStartNewTransaction() would raise an UnexpectedRollbackException
178
            endTransaction();
179
            startNewTransaction();
180
        }
181
        Assert.assertNotNull("Must fail here!", exception);
182

  
183
    }
184

  
185
    @Test
186
    @DataSet
187
    public void testUserService_CreateAllow(){
188

  
189
        authentication = authenticationManager.authenticate(tokenForUserManager);
190
        SecurityContext context = SecurityContextHolder.getContext();
191
        context.setAuthentication(authentication);
192

  
193
        RuntimeException exception = null;
194
        try {
195
            userService.createUser(User.NewInstance("new guy", "alkjdsfalkj"));
196
            commitAndStartNewTransaction(null);
197
        } catch (AccessDeniedException e){
198
            logger.error("Unexpected failure of evaluation.", e);
199
            exception = e;
200
        } catch (RuntimeException e){
201
            exception = findThrowableOfTypeIn(PermissionDeniedException.class, e);
202
            logger.error("unexpected failure of evaluation.", exception);
203
        } finally {
204
            // needed in case saveOrUpdate was interrupted by the RuntimeException
205
            // commitAndStartNewTransaction() would raise an UnexpectedRollbackException
206
            endTransaction();
207
            startNewTransaction();
208
        }
209
        Assert.assertNull("Must not fail here!", exception);
210

  
211
    }
212

  
213

  
214
    @Test
215
    @DataSet
216
    @Ignore // FIXME http://dev.e-taxonomy.eu/trac/ticket/3098
217
    public void testHasPermissions(){
218

  
219
        Taxon taxon = Taxon.NewInstance(BotanicalName.NewInstance(Rank.GENUS()),null);
220

  
221
        authentication = authenticationManager.authenticate(tokenForTaxonomist);
222
        boolean hasPermission = permissionEvaluator.hasPermission(authentication, taxon, Operation.ALL);
223
        assertTrue(hasPermission);
224
    }
225

  
226

  
227
    /**
228
     * Test method for {@link eu.etaxonomy.cdm.api.service.TaxonServiceImpl#saveTaxon(eu.etaxonomy.cdm.model.taxon.TaxonBase)}.
229
     */
230
    @Test
231
    public final void testSaveTaxon() {
232

  
233
        authentication = authenticationManager.authenticate(tokenForAdmin);
234
        SecurityContext context = SecurityContextHolder.getContext();
235
        context.setAuthentication(authentication);
236

  
237
        Taxon expectedTaxon = Taxon.NewInstance(BotanicalName.NewInstance(Rank.SPECIES()), null);
238
        expectedTaxon.getName().setTitleCache("Newby admin", true);
239
        UUID uuid = taxonService.save(expectedTaxon).getUuid();
240
        commitAndStartNewTransaction(null);
241
        TaxonBase<?> actualTaxon = taxonService.load(uuid);
242
        assertEquals(expectedTaxon, actualTaxon);
243

  
244
        authentication = authenticationManager.authenticate(tokenForTaxonEditor);
245
        context = SecurityContextHolder.getContext();
246
        context.setAuthentication(authentication);
247
        expectedTaxon = Taxon.NewInstance(BotanicalName.NewInstance(Rank.GENUS()), null);
248
        expectedTaxon.getName().setTitleCache("Newby taxonEditor", true);
249
        uuid = taxonService.saveOrUpdate(expectedTaxon);
250
        commitAndStartNewTransaction(null);
251
        actualTaxon = taxonService.load(uuid);
252
        assertEquals(expectedTaxon, actualTaxon);
253

  
254
    }
255

  
256
    @Test
257
    public final void testSaveNameAllow() {
258

  
259
        authentication = authenticationManager.authenticate(tokenForTaxonEditor);
260
        SecurityContext context = SecurityContextHolder.getContext();
261
        context.setAuthentication(authentication);
262

  
263
        ZoologicalName newName = ZoologicalName.NewInstance(Rank.SPECIES());
264
        newName.setTitleCache("Newby taxonEditor", true);
265
        UUID uuid = nameService.saveOrUpdate(newName);
266
        commitAndStartNewTransaction(null);
267
        TaxonNameBase savedName = nameService.load(uuid);
268
        assertEquals(newName, savedName);
269
    }
270

  
271

  
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff