comment on saveOrUpdate operation
[cdm-vaadin.git] / src / main / java / eu / etaxonomy / cdm / service / CdmStore.java
1 /**
2 * Copyright (C) 2017 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.service;
10
11 import org.apache.log4j.Logger;
12 import org.hibernate.Session;
13 import org.hibernate.engine.spi.SessionImplementor;
14 import org.springframework.transaction.TransactionStatus;
15 import org.springframework.transaction.support.DefaultTransactionDefinition;
16
17 import com.vaadin.ui.Notification;
18 import com.vaadin.ui.UI;
19
20 import eu.etaxonomy.cdm.api.application.CdmRepository;
21 import eu.etaxonomy.cdm.api.conversation.ConversationHolder;
22 import eu.etaxonomy.cdm.api.service.DeleteResult;
23 import eu.etaxonomy.cdm.api.service.IService;
24 import eu.etaxonomy.cdm.model.common.CdmBase;
25 import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
26 import eu.etaxonomy.cdm.vaadin.event.EntityChangeEvent;
27 import eu.etaxonomy.cdm.vaadin.event.EntityChangeEvent.Type;
28
29 /**
30 * @author a.kohlbecker
31 * @since Jun 26, 2017
32 *
33 * TODO better naming of this class, ServiceWrapper, ServiceOperator, ...?
34 *
35 */
36 public class CdmStore<T extends CdmBase, S extends IService<T>> {
37
38 private static final Logger logger = Logger.getLogger(CdmStore.class);
39
40 private CdmRepository repo;
41
42 private S service;
43
44 TransactionStatus txNonConversational = null;
45
46 ConversationHolder conversationHolder = null;
47
48 /**
49 * @return the conversationHolder
50 */
51 public ConversationHolder getConversationHolder() {
52 return conversationHolder;
53 }
54
55 protected DefaultTransactionDefinition txDefinition = null;
56
57 /**
58 *
59 * @param repo
60 * @param service
61 * may be <code>null</code>, but delete operations will fail with
62 * a NullPointerException in this case.
63 */
64 public CdmStore(CdmRepository repo, S service) {
65
66 this.repo = repo;
67 this.service = service;
68
69 }
70
71 /**
72 * constructor which takes a ConversationHolder. The supplying class of the conversationHolder needs
73 * to care for <code>bind()</code>, <code>unbind()</code> and <code>close()</code> since the store is
74 * only responsible for starting and committing of transactions.
75 *
76 * @param repo
77 * @param service
78 * @param conversationHolder
79 */
80 public CdmStore(CdmRepository repo, S service, ConversationHolder conversationHolder) {
81
82 this.repo = repo;
83 this.service = service;
84 this.conversationHolder = conversationHolder;
85
86 }
87
88 /**
89 * @return
90 *
91 */
92 public TransactionStatus startTransaction() {
93 if(conversationHolder != null && !conversationHolder.isTransactionActive()){
94 //conversationHolder.setDefinition(getTransactionDefinition());
95 return conversationHolder.startTransaction();
96 } else {
97 checkExistingTransaction();
98 txNonConversational = repo.startTransaction();
99 return txNonConversational;
100 }
101 }
102
103 /**
104 *
105 */
106 protected void checkExistingTransaction() {
107 if (txNonConversational != null) {
108 // @formatter:off
109 // holding the TransactionStatus as state is not good design. we
110 // should change the save operation
111 // in the EditorView so that the presenter can process the save in
112 // one method call.
113 // Problems:
114 // 1. the fieldGroup needs a open session and read transaction
115 // during the validation, otherwise
116 // LazyInitialisationExceptions occur.
117 // 2. passing the TransactionState to the view also doesn't seem
118 // like a good idea.
119 // @formatter:on
120 throw new RuntimeException("Opening a second transaction in the same" + this.getClass().getSimpleName() + " is not supported");
121 }
122 }
123
124 /**
125 * If the bean is contained in the session it is being updated by doing an
126 * evict and merge. The fieldGroup is updated with the merged bean.
127 *
128 *
129 * @param bean
130 * @return The bean merged to the session or original bean in case a merge
131 * was not necessary.
132 */
133 public T mergedBean(T bean) {
134
135 Session session = getSession();
136
137 // session.clear();
138 if (session.contains(bean)) {
139 // evict bean before merge to avoid duplicate beans in same session
140 logger.trace(this._toString() + ".mergedBean() - evict " + bean.toString());
141 session.evict(bean);
142 }
143
144 logger.trace(this._toString() + ".mergedBean() - doing merge of" + bean.toString());
145 // to avoid merge problems as described in
146 // https://dev.e-taxonomy.eu/redmine/issues/6687
147 // we are set the hibernate property
148 // hibernate.event.merge.entity_copy_observer=allow
149 @SuppressWarnings("unchecked")
150 T mergedBean = (T) session.merge(bean);
151 logger.trace(this._toString() + ".mergedBean() - bean after merge " + bean.toString());
152 return mergedBean;
153
154 }
155
156 /**
157 * @return
158 */
159 private Session getSession() {
160
161 Session session;
162 if(conversationHolder != null){
163 session = conversationHolder.getSession();
164 } else {
165 session = repo.getSession();
166 }
167 logger.trace(this._toString() + ".getSession() - session:" + session.hashCode() + ", persistenceContext: "
168 + ((SessionImplementor) session).getPersistenceContext() + " - " + session.toString());
169
170 return session;
171 }
172
173 protected String _toString() {
174 return this.getClass().getSimpleName() + "@" + this.hashCode();
175 }
176
177 /**
178 *
179 * @param bean
180
181 * @return the merged bean, this bean is <b>not reloaded</b> from the
182 * persistent storage.
183 */
184 public EntityChangeEvent saveBean(T bean) {
185
186 Type changeEventType;
187 if(bean.getId() > 1){
188 changeEventType = Type.MODIFIED;
189 } else {
190 changeEventType = Type.CREATED;
191 }
192
193 Session session = getSession();
194 logger.trace(this._toString() + ".onEditorSaveEvent - session: " + session.hashCode());
195
196 if(txNonConversational == null || (conversationHolder != null && !conversationHolder.isTransactionActive())){
197 // no running transaction, start one ...
198 startTransaction();
199 }
200
201 logger.trace(this._toString() + ".onEditorSaveEvent - merging bean into session");
202 // merge the changes into the session, ...
203
204 T mergedBean = mergedBean(bean);
205 //T mergedBean = bean;
206 // NOTE: saveOrUpdate is really needed here even if we to a merge before
207 repo.getCommonService().saveOrUpdate(mergedBean);
208 session.flush();
209 commitTransction();
210
211 return new EntityChangeEvent(mergedBean.getClass(), mergedBean.getId(), changeEventType);
212 }
213
214 /**
215 *
216 * @param bean
217 * @return a EntityChangeEvent in case the deletion was successful otherwise <code>null</code>.
218 */
219 public final EntityChangeEvent deleteBean(T bean) {
220
221 logger.trace(this._toString() + ".onEditorPreSaveEvent - starting transaction");
222
223 startTransaction();
224 logger.trace(this._toString() + ".deleteBean - deleting" + bean.toString());
225 DeleteResult result = service.delete(bean);
226 if (result.isOk()) {
227
228 getSession().flush();
229 commitTransction();
230 logger.trace(this._toString() + ".deleteBean - transaction comitted");
231 return new EntityChangeEvent(bean.getClass(), bean.getId(), Type.REMOVED);
232 } else {
233 String notificationTitle;
234 StringBuffer messageBody = new StringBuffer();
235 if (result.isAbort()) {
236 notificationTitle = "The delete operation as abborded by the system.";
237 } else {
238 notificationTitle = "An error occured during the delete operation.";
239 }
240 if (!result.getExceptions().isEmpty()) {
241 messageBody.append("<h3>").append("Exceptions:").append("</h3>").append("<ul>");
242 result.getExceptions().forEach(e -> messageBody.append("<li>").append(e.getMessage()).append("</li>"));
243 messageBody.append("</ul>");
244 }
245 if (!result.getRelatedObjects().isEmpty()) {
246 messageBody.append("<h3>").append("Related objects exist:").append("</h3>").append("<ul>");
247 result.getRelatedObjects().forEach(e -> {
248 messageBody.append("<li>");
249 if (IdentifiableEntity.class.isAssignableFrom(e.getClass())) {
250 messageBody.append(((IdentifiableEntity) e).getTitleCache());
251 } else {
252 messageBody.append(e.toString());
253 }
254 messageBody.append("</li>");
255 });
256
257 messageBody.append("</ul>");
258 }
259 Notification notification = new Notification(notificationTitle, messageBody.toString(),
260 com.vaadin.ui.Notification.Type.ERROR_MESSAGE, true);
261 notification.show(UI.getCurrent().getPage());
262 txNonConversational = null;
263 }
264 return null;
265 }
266
267
268 protected void commitTransction() {
269
270 if(conversationHolder != null){
271 conversationHolder.commit();
272 } else {
273 repo.commitTransaction(txNonConversational);
274 txNonConversational = null;
275 }
276 }
277
278 /**
279 * @param entityId
280 */
281 public T loadBean(int entityId) {
282 conversationHolder.startTransaction();
283 return service.find(entityId);
284 }
285
286 public S getService() {
287 return service;
288 }
289
290
291 }