View Javadoc
1   package org.argeo.cms.ui.forms;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.text.DateFormat;
6   import java.text.SimpleDateFormat;
7   import java.util.ArrayList;
8   import java.util.Calendar;
9   import java.util.List;
10  
11  import javax.jcr.Node;
12  import javax.jcr.RepositoryException;
13  import javax.jcr.Session;
14  import javax.jcr.Value;
15  import javax.jcr.ValueFormatException;
16  
17  import org.apache.commons.logging.Log;
18  import org.apache.commons.logging.LogFactory;
19  import org.argeo.cms.CmsException;
20  import org.argeo.cms.ui.CmsEditable;
21  import org.argeo.cms.ui.CmsImageManager;
22  import org.argeo.cms.ui.util.CmsUiUtils;
23  import org.argeo.cms.ui.viewers.AbstractPageViewer;
24  import org.argeo.cms.ui.viewers.EditablePart;
25  import org.argeo.cms.ui.viewers.Section;
26  import org.argeo.cms.ui.viewers.SectionPart;
27  import org.argeo.cms.ui.widgets.EditableImage;
28  import org.argeo.cms.ui.widgets.Img;
29  import org.argeo.cms.ui.widgets.StyledControl;
30  import org.argeo.eclipse.ui.EclipseUiUtils;
31  import org.argeo.jcr.JcrUtils;
32  import org.eclipse.jface.dialogs.MessageDialog;
33  import org.eclipse.rap.fileupload.FileDetails;
34  import org.eclipse.rap.fileupload.FileUploadEvent;
35  import org.eclipse.rap.fileupload.FileUploadHandler;
36  import org.eclipse.rap.fileupload.FileUploadListener;
37  import org.eclipse.rap.fileupload.FileUploadReceiver;
38  import org.eclipse.rap.rwt.service.ServerPushSession;
39  import org.eclipse.rap.rwt.widgets.FileUpload;
40  import org.eclipse.swt.SWT;
41  import org.eclipse.swt.events.FocusEvent;
42  import org.eclipse.swt.events.FocusListener;
43  import org.eclipse.swt.events.MouseAdapter;
44  import org.eclipse.swt.events.MouseEvent;
45  import org.eclipse.swt.events.MouseListener;
46  import org.eclipse.swt.events.SelectionAdapter;
47  import org.eclipse.swt.events.SelectionEvent;
48  import org.eclipse.swt.events.SelectionListener;
49  import org.eclipse.swt.graphics.Point;
50  import org.eclipse.swt.layout.FormAttachment;
51  import org.eclipse.swt.layout.FormData;
52  import org.eclipse.swt.layout.FormLayout;
53  import org.eclipse.swt.layout.GridData;
54  import org.eclipse.swt.layout.GridLayout;
55  import org.eclipse.swt.layout.RowLayout;
56  import org.eclipse.swt.widgets.Button;
57  import org.eclipse.swt.widgets.Composite;
58  import org.eclipse.swt.widgets.Control;
59  import org.eclipse.swt.widgets.Label;
60  import org.eclipse.swt.widgets.Text;
61  
62  /** Manage life cycle of a form page that is linked to a given node */
63  public class FormPageViewer extends AbstractPageViewer {
64  	private final static Log log = LogFactory.getLog(FormPageViewer.class);
65  	private static final long serialVersionUID = 5277789504209413500L;
66  
67  	private final Section mainSection;
68  
69  	// TODO manage within the CSS
70  	private int labelColWidth = 150;
71  	private int rowLayoutHSpacing = 8;
72  
73  	// Context cached in the viewer
74  	// The reference to translate from text to calendar and reverse
75  	private DateFormat dateFormat = new SimpleDateFormat(FormUtils.DEFAULT_SHORT_DATE_FORMAT);
76  	private CmsImageManager imageManager;
77  	private FileUploadListener fileUploadListener;
78  
79  	public FormPageViewer(Section mainSection, int style, CmsEditable cmsEditable) throws RepositoryException {
80  		super(mainSection, style, cmsEditable);
81  		this.mainSection = mainSection;
82  
83  		if (getCmsEditable().canEdit()) {
84  			fileUploadListener = new FUL();
85  		}
86  	}
87  
88  	@Override
89  	protected void prepare(EditablePart part, Object caretPosition) {
90  		if (part instanceof Img) {
91  			((Img) part).setFileUploadListener(fileUploadListener);
92  		}
93  	}
94  
95  	/** To be overridden.Save the edited part. */
96  	protected void save(EditablePart part) throws RepositoryException {
97  		Node node = null;
98  		if (part instanceof EditableMultiStringProperty) {
99  			EditableMultiStringProperty ept = (EditableMultiStringProperty) part;
100 			// SWT : View
101 			List<String> values = ept.getValues();
102 			// JCR : Model
103 			node = ept.getNode();
104 			String propName = ept.getPropertyName();
105 			if (values.isEmpty()) {
106 				if (node.hasProperty(propName))
107 					node.getProperty(propName).remove();
108 			} else {
109 				node.setProperty(propName, values.toArray(new String[0]));
110 			}
111 			// => Viewer : Controller
112 		} else if (part instanceof EditablePropertyString) {
113 			EditablePropertyString ept = (EditablePropertyString) part;
114 			// SWT : View
115 			String txt = ((Text) ept.getControl()).getText();
116 			// JCR : Model
117 			node = ept.getNode();
118 			String propName = ept.getPropertyName();
119 			if (EclipseUiUtils.isEmpty(txt)) {
120 				if (node.hasProperty(propName))
121 					node.getProperty(propName).remove();
122 			} else {
123 				setPropertySilently(node, propName, txt);
124 				// node.setProperty(propName, txt);
125 			}
126 			// node.getSession().save();
127 			// => Viewer : Controller
128 		} else if (part instanceof EditablePropertyDate) {
129 			EditablePropertyDate ept = (EditablePropertyDate) part;
130 			Calendar cal = FormUtils.parseDate(dateFormat, ((Text) ept.getControl()).getText());
131 			node = ept.getNode();
132 			String propName = ept.getPropertyName();
133 			if (cal == null) {
134 				if (node.hasProperty(propName))
135 					node.getProperty(propName).remove();
136 			} else {
137 				node.setProperty(propName, cal);
138 			}
139 			// node.getSession().save();
140 			// => Viewer : Controller
141 		}
142 		// TODO: make this configurable, sometimes we do not want to save the
143 		// current session at this stage
144 		if (node != null && node.getSession().hasPendingChanges()) {
145 			JcrUtils.updateLastModified(node);
146 			node.getSession().save();
147 		}
148 	}
149 
150 	@Override
151 	protected void updateContent(EditablePart part) throws RepositoryException {
152 		if (part instanceof EditableMultiStringProperty) {
153 			EditableMultiStringProperty ept = (EditableMultiStringProperty) part;
154 			// SWT : View
155 			Node node = ept.getNode();
156 			String propName = ept.getPropertyName();
157 			List<String> valStrings = new ArrayList<String>();
158 			if (node.hasProperty(propName)) {
159 				Value[] values = node.getProperty(propName).getValues();
160 				for (Value val : values)
161 					valStrings.add(val.getString());
162 			}
163 			ept.setValues(valStrings);
164 		} else if (part instanceof EditablePropertyString) {
165 			// || part instanceof EditableLink
166 			EditablePropertyString ept = (EditablePropertyString) part;
167 			// JCR : Model
168 			Node node = ept.getNode();
169 			String propName = ept.getPropertyName();
170 			if (node.hasProperty(propName)) {
171 				String value = node.getProperty(propName).getString();
172 				ept.setText(value);
173 			} else
174 				ept.setText("");
175 			// => Viewer : Controller
176 		} else if (part instanceof EditablePropertyDate) {
177 			EditablePropertyDate ept = (EditablePropertyDate) part;
178 			// JCR : Model
179 			Node node = ept.getNode();
180 			String propName = ept.getPropertyName();
181 			if (node.hasProperty(propName))
182 				ept.setText(dateFormat.format(node.getProperty(propName).getDate().getTime()));
183 			else
184 				ept.setText("");
185 		} else if (part instanceof SectionPart) {
186 			SectionPart sectionPart = (SectionPart) part;
187 			Node partNode = sectionPart.getNode();
188 			// use control AFTER setting style, since it may have been reset
189 			if (part instanceof EditableImage) {
190 				EditableImage editableImage = (EditableImage) part;
191 				imageManager().load(partNode, part.getControl(), editableImage.getPreferredImageSize());
192 			}
193 		}
194 	}
195 
196 	// FILE UPLOAD LISTENER
197 	protected class FUL implements FileUploadListener {
198 
199 		public FUL() {
200 		}
201 
202 		public void uploadProgress(FileUploadEvent event) {
203 			// TODO Monitor upload progress
204 		}
205 
206 		public void uploadFailed(FileUploadEvent event) {
207 			throw new CmsException("Upload failed " + event, event.getException());
208 		}
209 
210 		public void uploadFinished(FileUploadEvent event) {
211 			for (FileDetails file : event.getFileDetails()) {
212 				if (log.isDebugEnabled())
213 					log.debug("Received: " + file.getFileName());
214 			}
215 			mainSection.getDisplay().syncExec(new Runnable() {
216 				@Override
217 				public void run() {
218 					saveEdit();
219 				}
220 			});
221 			FileUploadHandler uploadHandler = (FileUploadHandler) event.getSource();
222 			uploadHandler.dispose();
223 		}
224 	}
225 
226 	// FOCUS OUT LISTENER
227 	protected FocusListener createFocusListener() {
228 		return new FocusOutListener();
229 	}
230 
231 	private class FocusOutListener implements FocusListener {
232 		private static final long serialVersionUID = -6069205786732354186L;
233 
234 		@Override
235 		public void focusLost(FocusEvent event) {
236 			saveEdit();
237 		}
238 
239 		@Override
240 		public void focusGained(FocusEvent event) {
241 			// does nothing;
242 		}
243 	}
244 
245 	// MOUSE LISTENER
246 	@Override
247 	protected MouseListener createMouseListener() {
248 		return new ML();
249 	}
250 
251 	private class ML extends MouseAdapter {
252 		private static final long serialVersionUID = 8526890859876770905L;
253 
254 		@Override
255 		public void mouseDoubleClick(MouseEvent e) {
256 			if (e.button == 1) {
257 				Control source = (Control) e.getSource();
258 				if (getCmsEditable().canEdit()) {
259 					if (getCmsEditable().isEditing() && !(getEdited() instanceof Img)) {
260 						if (source == mainSection)
261 							return;
262 						EditablePart part = findDataParent(source);
263 						upload(part);
264 					} else {
265 						getCmsEditable().startEditing();
266 					}
267 				}
268 			}
269 		}
270 
271 		@Override
272 		public void mouseDown(MouseEvent e) {
273 			if (getCmsEditable().isEditing()) {
274 				if (e.button == 1) {
275 					Control source = (Control) e.getSource();
276 					EditablePart composite = findDataParent(source);
277 					Point point = new Point(e.x, e.y);
278 					if (!(composite instanceof Img))
279 						edit(composite, source.toDisplay(point));
280 				} else if (e.button == 3) {
281 					// EditablePart composite = findDataParent((Control) e
282 					// .getSource());
283 					// if (styledTools != null)
284 					// styledTools.show(composite, new Point(e.x, e.y));
285 				}
286 			}
287 		}
288 
289 		protected synchronized void upload(EditablePart part) {
290 			if (part instanceof SectionPart) {
291 				if (part instanceof Img) {
292 					if (getEdited() == part)
293 						return;
294 					edit(part, null);
295 					layout(part.getControl());
296 				}
297 			}
298 		}
299 	}
300 
301 	@Override
302 	public Control getControl() {
303 		return mainSection;
304 	}
305 
306 	protected CmsImageManager imageManager() {
307 		if (imageManager == null)
308 			imageManager = CmsUiUtils.getCmsView().getImageManager();
309 		return imageManager;
310 	}
311 
312 	// LOCAL UI HELPERS
313 	protected Section createSectionIfNeeded(Composite body, Node node) throws RepositoryException {
314 		Section section = null;
315 		if (node != null) {
316 			section = new Section(body, SWT.NO_FOCUS, node);
317 			section.setLayoutData(CmsUiUtils.fillWidth());
318 			section.setLayout(CmsUiUtils.noSpaceGridLayout());
319 		}
320 		return section;
321 	}
322 
323 	protected void createSimpleLT(Composite bodyRow, Node node, String propName, String label, String msg)
324 			throws RepositoryException {
325 		if (getCmsEditable().canEdit() || node.hasProperty(propName)) {
326 			createPropertyLbl(bodyRow, label);
327 			EditablePropertyString eps = new EditablePropertyString(bodyRow, SWT.WRAP | SWT.LEFT, node, propName, msg);
328 			eps.setMouseListener(getMouseListener());
329 			eps.setFocusListener(getFocusListener());
330 			eps.setLayoutData(CmsUiUtils.fillWidth());
331 		}
332 	}
333 
334 	protected void createMultiStringLT(Composite bodyRow, Node node, String propName, String label, String msg)
335 			throws RepositoryException {
336 		boolean canEdit = getCmsEditable().canEdit();
337 		if (canEdit || node.hasProperty(propName)) {
338 			createPropertyLbl(bodyRow, label);
339 
340 			List<String> valueStrings = new ArrayList<String>();
341 
342 			if (node.hasProperty(propName)) {
343 				Value[] values = node.getProperty(propName).getValues();
344 				for (Value value : values)
345 					valueStrings.add(value.getString());
346 			}
347 
348 			// TODO use a drop down to display possible values to the end user
349 			EditableMultiStringProperty emsp = new EditableMultiStringProperty(bodyRow, SWT.SINGLE | SWT.LEAD, node,
350 					propName, valueStrings, new String[] { "Implement this" }, msg,
351 					canEdit ? getRemoveValueSelListener() : null);
352 			addListeners(emsp);
353 			// emsp.setMouseListener(getMouseListener());
354 			emsp.setStyle(FormStyle.propertyMessage.style());
355 			emsp.setLayoutData(CmsUiUtils.fillWidth());
356 		}
357 	}
358 
359 	protected Label createPropertyLbl(Composite parent, String value) {
360 		return createPropertyLbl(parent, value, SWT.TOP);
361 	}
362 
363 	protected Label createPropertyLbl(Composite parent, String value, int vAlign) {
364 		boolean isSmall = CmsUiUtils.getCmsView().getUxContext().isSmall();
365 		Label label = new Label(parent, isSmall ? SWT.LEFT : SWT.RIGHT | SWT.WRAP);
366 		label.setText(value + " ");
367 		CmsUiUtils.style(label, FormStyle.propertyLabel.style());
368 		GridData gd = new GridData(isSmall ? SWT.LEFT : SWT.RIGHT, vAlign, false, false);
369 		gd.widthHint = labelColWidth;
370 		label.setLayoutData(gd);
371 		return label;
372 	}
373 
374 	protected Label newStyledLabel(Composite parent, String style, String value) {
375 		Label label = new Label(parent, SWT.NONE);
376 		label.setText(value);
377 		CmsUiUtils.style(label, style);
378 		return label;
379 	}
380 
381 	protected Composite createRowLayoutComposite(Composite parent) throws RepositoryException {
382 		Composite bodyRow = new Composite(parent, SWT.NO_FOCUS);
383 		bodyRow.setLayoutData(CmsUiUtils.fillWidth());
384 		RowLayout rl = new RowLayout(SWT.WRAP);
385 		rl.type = SWT.HORIZONTAL;
386 		rl.spacing = rowLayoutHSpacing;
387 		rl.marginHeight = rl.marginWidth = 0;
388 		rl.marginTop = rl.marginBottom = rl.marginLeft = rl.marginRight = 0;
389 		bodyRow.setLayout(rl);
390 		return bodyRow;
391 	}
392 
393 	protected Composite createAddImgComposite(final Section section, Composite parent, final Node parentNode)
394 			throws RepositoryException {
395 
396 		Composite body = new Composite(parent, SWT.NO_FOCUS);
397 		body.setLayout(new GridLayout());
398 
399 		FormFileUploadReceiver receiver = new FormFileUploadReceiver(section, parentNode, null);
400 		final FileUploadHandler currentUploadHandler = new FileUploadHandler(receiver);
401 		if (fileUploadListener != null)
402 			currentUploadHandler.addUploadListener(fileUploadListener);
403 
404 		// Button creation
405 		final FileUpload fileUpload = new FileUpload(body, SWT.BORDER);
406 		fileUpload.setText("Import an image");
407 		fileUpload.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
408 		fileUpload.addSelectionListener(new SelectionAdapter() {
409 			private static final long serialVersionUID = 4869523412991968759L;
410 
411 			@Override
412 			public void widgetSelected(SelectionEvent e) {
413 				ServerPushSession pushSession = new ServerPushSession();
414 				pushSession.start();
415 				String uploadURL = currentUploadHandler.getUploadUrl();
416 				fileUpload.submit(uploadURL);
417 			}
418 		});
419 
420 		return body;
421 	}
422 
423 	protected class FormFileUploadReceiver extends FileUploadReceiver {
424 
425 		private Node context;
426 		private Section section;
427 		private String name;
428 
429 		public FormFileUploadReceiver(Section section, Node context, String name) {
430 			this.context = context;
431 			this.section = section;
432 			this.name = name;
433 		}
434 
435 		@Override
436 		public void receive(InputStream stream, FileDetails details) throws IOException {
437 
438 			if (name == null)
439 				name = details.getFileName();
440 
441 			// TODO clean image name more carefully
442 			String cleanedName = name.replaceAll("[^a-zA-Z0-9-.]", "");
443 			// We add a unique prefix to workaround the cache issue: when
444 			// deleting and re-adding a new image with same name, the end user
445 			// browser will use the cache and the image will remain unchanged
446 			// for a while
447 			cleanedName = System.currentTimeMillis() % 100000 + "_" + cleanedName;
448 
449 			try {
450 				imageManager().uploadImage(context, cleanedName, stream);
451 				// TODO clean refresh strategy
452 				section.getDisplay().asyncExec(new Runnable() {
453 					@Override
454 					public void run() {
455 						try {
456 							FormPageViewer.this.refresh(section);
457 							section.layout();
458 							section.getParent().layout();
459 						} catch (RepositoryException re) {
460 							throw new CmsException("unable to refresh " + "image section for " + context);
461 						}
462 					}
463 				});
464 			} catch (RepositoryException re) {
465 				throw new CmsException("unable to upload image " + name + " at " + context);
466 			}
467 		}
468 	}
469 
470 	protected void addListeners(StyledControl control) {
471 		control.setMouseListener(getMouseListener());
472 		control.setFocusListener(getFocusListener());
473 	}
474 
475 	protected Img createImgComposite(Composite parent, Node node, Point preferredSize) throws RepositoryException {
476 		Img img = new Img(parent, SWT.NONE, node, preferredSize) {
477 			private static final long serialVersionUID = 1297900641952417540L;
478 
479 			@Override
480 			protected void setContainerLayoutData(Composite composite) {
481 				composite.setLayoutData(CmsUiUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
482 			}
483 
484 			@Override
485 			protected void setControlLayoutData(Control control) {
486 				control.setLayoutData(CmsUiUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
487 			}
488 		};
489 		img.setLayoutData(CmsUiUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
490 		updateContent(img);
491 		addListeners(img);
492 		return img;
493 	}
494 
495 	protected Composite addDeleteAbility(final Section section, final Node sessionNode, int topWeight,
496 			int rightWeight) {
497 		Composite comp = new Composite(section, SWT.NONE);
498 		comp.setLayoutData(CmsUiUtils.fillAll());
499 		comp.setLayout(new FormLayout());
500 
501 		// The body to be populated
502 		Composite body = new Composite(comp, SWT.NO_FOCUS);
503 		body.setLayoutData(EclipseUiUtils.fillFormData());
504 
505 		if (getCmsEditable().canEdit()) {
506 			// the delete button
507 			Button deleteBtn = new Button(comp, SWT.FLAT);
508 			CmsUiUtils.style(deleteBtn, FormStyle.deleteOverlay.style());
509 			FormData formData = new FormData();
510 			formData.right = new FormAttachment(rightWeight, 0);
511 			formData.top = new FormAttachment(topWeight, 0);
512 			deleteBtn.setLayoutData(formData);
513 			deleteBtn.moveAbove(body);
514 
515 			deleteBtn.addSelectionListener(new SelectionAdapter() {
516 				private static final long serialVersionUID = 4304223543657238462L;
517 
518 				@Override
519 				public void widgetSelected(SelectionEvent e) {
520 					super.widgetSelected(e);
521 					if (MessageDialog.openConfirm(section.getShell(), "Confirm deletion",
522 							"Are you really you want to remove this?")) {
523 						Session session;
524 						try {
525 							session = sessionNode.getSession();
526 							Section parSection = section.getParentSection();
527 							sessionNode.remove();
528 							session.save();
529 							refresh(parSection);
530 							layout(parSection);
531 						} catch (RepositoryException re) {
532 							throw new CmsException("Unable to delete " + sessionNode, re);
533 						}
534 
535 					}
536 
537 				}
538 			});
539 		}
540 		return body;
541 	}
542 
543 //	// LOCAL HELPERS FOR NODE MANAGEMENT
544 //	private Node getOrCreateNode(Node parent, String nodeName, String nodeType) throws RepositoryException {
545 //		Node node = null;
546 //		if (getCmsEditable().canEdit() && !parent.hasNode(nodeName)) {
547 //			node = JcrUtils.mkdirs(parent, nodeName, nodeType);
548 //			parent.getSession().save();
549 //		}
550 //
551 //		if (getCmsEditable().canEdit() || parent.hasNode(nodeName))
552 //			node = parent.getNode(nodeName);
553 //
554 //		return node;
555 //	}
556 
557 	private SelectionListener getRemoveValueSelListener() {
558 		return new SelectionAdapter() {
559 			private static final long serialVersionUID = 9022259089907445195L;
560 
561 			@Override
562 			public void widgetSelected(SelectionEvent e) {
563 				Object source = e.getSource();
564 				if (source instanceof Button) {
565 					Button btn = (Button) source;
566 					Object obj = btn.getData(FormConstants.LINKED_VALUE);
567 					EditablePart ep = findDataParent(btn);
568 					if (ep != null && ep instanceof EditableMultiStringProperty) {
569 						EditableMultiStringProperty emsp = (EditableMultiStringProperty) ep;
570 						List<String> values = emsp.getValues();
571 						if (values.contains(obj)) {
572 							values.remove(values.indexOf(obj));
573 							emsp.setValues(values);
574 							try {
575 								save(emsp);
576 								// TODO workaround to force refresh
577 								edit(emsp, 0);
578 								cancelEdit();
579 							} catch (RepositoryException e1) {
580 								throw new CmsException("Unable to remove value " + obj, e1);
581 							}
582 							layout(emsp);
583 						}
584 					}
585 				}
586 			}
587 		};
588 	}
589 
590 	protected void setPropertySilently(Node node, String propName, String value) throws RepositoryException {
591 		try {
592 			// TODO Clean this:
593 			// Format strings to replace \n
594 			value = value.replaceAll("\n", "<br/>");
595 			// Do not make the update if validation fails
596 			try {
597 				MarkupValidatorCopy.getInstance().validate(value);
598 			} catch (Exception e) {
599 				log.warn("Cannot set [" + value + "] on prop " + propName + "of " + node
600 						+ ", String cannot be validated - " + e.getMessage());
601 				return;
602 			}
603 			// TODO check if the newly created property is of the correct type,
604 			// otherwise the property will be silently created with a STRING
605 			// property type.
606 			node.setProperty(propName, value);
607 		} catch (ValueFormatException vfe) {
608 			log.warn("Cannot set [" + value + "] on prop " + propName + "of " + node + " - " + vfe.getMessage());
609 		}
610 	}
611 }