This project has retired. For details please refer to its
Attic page.
JcrNode xref
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.apache.chemistry.opencmis.jcr;
21
22 import org.apache.chemistry.opencmis.commons.PropertyIds;
23 import org.apache.chemistry.opencmis.commons.data.AllowableActions;
24 import org.apache.chemistry.opencmis.commons.data.ObjectData;
25 import org.apache.chemistry.opencmis.commons.data.Properties;
26 import org.apache.chemistry.opencmis.commons.data.PropertyData;
27 import org.apache.chemistry.opencmis.commons.definitions.DocumentTypeDefinition;
28 import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
29 import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
30 import org.apache.chemistry.opencmis.commons.enums.Action;
31 import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
32 import org.apache.chemistry.opencmis.commons.enums.Updatability;
33 import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
34 import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
35 import org.apache.chemistry.opencmis.commons.exceptions.CmisNameConstraintViolationException;
36 import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
37 import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
38 import org.apache.chemistry.opencmis.commons.exceptions.CmisStorageException;
39 import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
40 import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl;
41 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
42 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
43 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyBooleanImpl;
44 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDateTimeImpl;
45 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
46 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIntegerImpl;
47 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
48 import org.apache.chemistry.opencmis.commons.impl.server.ObjectInfoImpl;
49 import org.apache.chemistry.opencmis.commons.server.ObjectInfoHandler;
50 import org.apache.chemistry.opencmis.jcr.type.JcrTypeHandlerManager;
51 import org.apache.chemistry.opencmis.jcr.util.Util;
52 import org.apache.commons.logging.Log;
53 import org.apache.commons.logging.LogFactory;
54
55 import javax.jcr.ItemNotFoundException;
56 import javax.jcr.Node;
57 import javax.jcr.PathNotFoundException;
58 import javax.jcr.Property;
59 import javax.jcr.RepositoryException;
60 import javax.jcr.Session;
61 import javax.jcr.version.Version;
62 import javax.jcr.version.VersionHistory;
63 import javax.jcr.version.VersionManager;
64 import java.math.BigInteger;
65 import java.util.ArrayList;
66 import java.util.Calendar;
67 import java.util.GregorianCalendar;
68 import java.util.HashSet;
69 import java.util.List;
70 import java.util.Set;
71
72
73
74
75
76 public abstract class JcrNode {
77
78 private static final Log log = LogFactory.getLog(JcrNode.class);
79
80
81
82
83 public static final String USER_UNKNOWN = "unknown";
84
85
86
87
88
89 public static final GregorianCalendar DATE_UNKNOWN;
90
91 static {
92 DATE_UNKNOWN = new GregorianCalendar();
93 DATE_UNKNOWN.setTimeInMillis(0);
94 }
95
96 private final Node node;
97 protected final JcrTypeManager typeManager;
98 protected final PathManager pathManager;
99 protected final JcrTypeHandlerManager typeHandlerManager;
100
101
102
103
104
105
106
107
108
109 protected JcrNode(Node node, JcrTypeManager typeManager, PathManager pathManager, JcrTypeHandlerManager typeHandlerManager) {
110 this.node = node;
111 this.typeManager = typeManager;
112 this.pathManager = pathManager;
113 this.typeHandlerManager = typeHandlerManager;
114 }
115
116
117
118
119 public Node getNode() {
120 return node;
121 }
122
123
124
125
126
127 public String getName() {
128 try {
129 return getNodeName();
130 }
131 catch (RepositoryException e) {
132 log.debug(e.getMessage(), e);
133 throw new CmisRuntimeException(e.getMessage(), e);
134 }
135 }
136
137
138
139
140
141 public String getId() {
142 try {
143 return getObjectId();
144 }
145 catch (RepositoryException e) {
146 log.debug(e.getMessage(), e);
147 throw new CmisRuntimeException(e.getMessage(), e);
148 }
149 }
150
151
152
153
154 public String getTypeId() {
155 return getTypeIdInternal();
156 }
157
158
159
160
161 public boolean isRoot() {
162 return pathManager.isRoot(node);
163 }
164
165
166
167
168 public boolean isDocument() {
169 return BaseTypeId.CMIS_DOCUMENT == getBaseTypeId();
170 }
171
172
173
174
175 public boolean isFolder() {
176 return BaseTypeId.CMIS_FOLDER == getBaseTypeId();
177 }
178
179
180
181
182 public boolean isVersionable() {
183 TypeDefinition typeDef = typeManager.getType(getTypeIdInternal());
184 return typeDef instanceof DocumentTypeDefinition
185 ? ((DocumentTypeDefinition) typeDef).isVersionable()
186 : false;
187 }
188
189
190
191
192
193 public JcrDocument asDocument() {
194 if (isDocument()) {
195 return (JcrDocument) this;
196 }
197 else {
198 throw new CmisConstraintException("Not a document: " + this);
199 }
200 }
201
202
203
204
205
206 public JcrFolder asFolder() {
207 if (isFolder()) {
208 return (JcrFolder) this;
209 }
210 else {
211 throw new CmisObjectNotFoundException("Not a folder: " + this);
212 }
213 }
214
215
216
217
218
219 public JcrVersionBase asVersion() {
220 if (isVersionable()) {
221 return (JcrVersionBase) this;
222 }
223 else {
224 throw new CmisObjectNotFoundException("Not a version: " + this);
225 }
226 }
227
228
229
230
231
232
233
234
235
236 public JcrNode getNode(String path) {
237 try {
238 return create(node.getNode(path));
239 }
240 catch (PathNotFoundException e) {
241 log.debug(e.getMessage(), e);
242 throw new CmisObjectNotFoundException(e.getMessage(), e);
243 }
244 catch (RepositoryException e) {
245 log.debug(e.getMessage(), e);
246 throw new CmisRuntimeException(e.getMessage(), e);
247 }
248 }
249
250
251
252
253 public ObjectData compileObjectType(Set<String> filter, Boolean includeAllowableActions,
254 ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
255
256 try {
257 ObjectDataImpl result = new ObjectDataImpl();
258 ObjectInfoImpl objectInfo = new ObjectInfoImpl();
259
260 PropertiesImpl properties = new PropertiesImpl();
261 filter = filter == null ? null : new HashSet<String>(filter);
262 compileProperties(properties, filter, objectInfo);
263 result.setProperties(properties);
264 if (filter != null && !filter.isEmpty()) {
265 log.debug("Unknown filter properties: " + filter.toString());
266 }
267
268 if (Boolean.TRUE.equals(includeAllowableActions)) {
269 result.setAllowableActions(getAllowableActions());
270 }
271
272 if (requiresObjectInfo) {
273 objectInfo.setObject(result);
274 objectInfos.addObjectInfo(objectInfo);
275 }
276
277 return result;
278 }
279 catch (RepositoryException e) {
280 log.debug(e.getMessage(), e);
281 throw new CmisRuntimeException(e.getMessage(), e);
282 }
283 }
284
285
286
287
288 public AllowableActions getAllowableActions() {
289 AllowableActionsImpl aas = new AllowableActionsImpl();
290 aas.setAllowableActions(compileAllowableActions(new HashSet<Action>()));
291 return aas;
292 }
293
294
295
296
297
298
299
300
301 public JcrFolder getParent() {
302 try {
303 return create(node.getParent()).asFolder();
304 }
305 catch (ItemNotFoundException e) {
306 log.debug(e.getMessage(), e);
307 throw new CmisObjectNotFoundException(e.getMessage(), e);
308 }
309 catch (RepositoryException e) {
310 log.debug(e.getMessage(), e);
311 throw new CmisRuntimeException(e.getMessage(), e);
312 }
313 }
314
315
316
317
318
319
320 public JcrNode updateProperties(Properties properties) {
321
322 String newName = PropertyHelper.getStringProperty(properties, PropertyIds.NAME);
323 boolean rename = newName != null && !getName().equals(newName);
324 if (rename && !JcrConverter.isValidJcrName(newName)) {
325 throw new CmisNameConstraintViolationException("Name is not valid: " + newName);
326 }
327 if (rename && isRoot()) {
328 throw new CmisUpdateConflictException("Cannot rename root node");
329 }
330
331 try {
332
333 Session session = getNode().getSession();
334 Node newNode;
335 if (rename) {
336 String destPath = PathManager.createCmisPath(node.getParent().getPath(), newName);
337 session.move(node.getPath(), destPath);
338 newNode = session.getNode(destPath);
339 }
340 else {
341 newNode = node;
342 }
343
344
345 PropertyUpdater propertyUpdater = PropertyUpdater.create(typeManager, getTypeId(), properties);
346
347 JcrVersionBase jcrVersion = isVersionable()
348 ? asVersion()
349 : null;
350
351
352 boolean autoCheckout = false;
353 if (!propertyUpdater.isEmpty()) {
354 autoCheckout = jcrVersion != null && !jcrVersion.isCheckedOut();
355 if (autoCheckout) {
356 jcrVersion.checkout();
357 }
358
359
360 propertyUpdater.apply(node);
361 }
362
363 session.save();
364
365 if (autoCheckout) {
366
367 return jcrVersion.checkin(null, null, "auto checkout");
368 }
369 else if (jcrVersion != null && jcrVersion.isCheckedOut()) {
370
371 JcrVersionBase jcrNewVersion = create(newNode).asVersion();
372 return jcrNewVersion.getPwc();
373 }
374 else {
375
376 return create(newNode);
377 }
378 }
379 catch (RepositoryException e) {
380 log.debug(e.getMessage(), e);
381 throw new CmisStorageException(e.getMessage(), e);
382 }
383
384 }
385
386
387
388
389
390
391 public void delete(boolean allVersions, boolean isPwc) {
392 try {
393 Session session = getNode().getSession();
394 getNode().remove();
395 session.save();
396 }
397 catch (RepositoryException e) {
398 log.debug(e.getMessage(), e);
399 throw new CmisRuntimeException(e.getMessage(), e);
400 }
401 }
402
403
404
405
406
407
408 public JcrNode move(JcrFolder parent) {
409 try {
410
411 String destPath = PathManager.createCmisPath(parent.getNode().getPath(), node.getName());
412 String srcPath = node.getPath();
413 Node newNode;
414 if (srcPath.equals(destPath)) {
415 newNode = node;
416 }
417 else {
418 Session session = getNode().getSession();
419 session.move(srcPath, destPath);
420 newNode = session.getNode(destPath);
421 session.save();
422 }
423
424 return create(newNode);
425 }
426 catch (RepositoryException e) {
427 log.debug(e.getMessage(), e);
428 throw new CmisStorageException(e.getMessage(), e);
429 }
430 }
431
432 @Override
433 public String toString() {
434 try {
435 return node.getPath();
436 }
437 catch (RepositoryException e) {
438 log.debug(e.getMessage(), e);
439 return e.getMessage();
440 }
441 }
442
443
444
445
446
447
448
449
450
451
452
453 protected abstract Node getContextNode() throws RepositoryException;
454
455
456
457
458 protected abstract BaseTypeId getBaseTypeId();
459
460
461
462
463 protected abstract String getTypeIdInternal();
464
465
466
467
468
469
470
471
472
473
474 protected void compileProperties(PropertiesImpl properties, Set<String> filter, ObjectInfoImpl objectInfo)
475 throws RepositoryException {
476
477 String typeId = getTypeIdInternal();
478 BaseTypeId baseTypeId = getBaseTypeId();
479
480 objectInfo.setBaseType(baseTypeId);
481 objectInfo.setTypeId(typeId);
482 objectInfo.setHasAcl(false);
483 objectInfo.setVersionSeriesId(getVersionSeriesId());
484 objectInfo.setRelationshipSourceIds(null);
485 objectInfo.setRelationshipTargetIds(null);
486 objectInfo.setRenditionInfos(null);
487 objectInfo.setSupportsPolicies(false);
488 objectInfo.setSupportsRelationships(false);
489
490
491 String objectId = getObjectId();
492 addPropertyId(properties, typeId, filter, PropertyIds.OBJECT_ID, objectId);
493 objectInfo.setId(objectId);
494
495
496 String name = getNodeName();
497 addPropertyString(properties, typeId, filter, PropertyIds.NAME, name);
498 objectInfo.setName(name);
499
500
501 addPropertyId(properties, typeId, filter, PropertyIds.BASE_TYPE_ID, baseTypeId.value());
502 addPropertyId(properties, typeId, filter, PropertyIds.OBJECT_TYPE_ID, typeId);
503
504
505 String createdBy = getCreatedBy();
506 addPropertyString(properties, typeId, filter, PropertyIds.CREATED_BY, createdBy);
507 objectInfo.setCreatedBy(createdBy);
508
509 addPropertyString(properties, typeId, filter, PropertyIds.LAST_MODIFIED_BY, getLastModifiedBy());
510
511
512 GregorianCalendar created = getCreated();
513 addPropertyDateTime(properties, typeId, filter, PropertyIds.CREATION_DATE, created);
514 objectInfo.setCreationDate(created);
515
516 GregorianCalendar lastModified = getLastModified();
517 addPropertyDateTime(properties, typeId, filter, PropertyIds.LAST_MODIFICATION_DATE, lastModified);
518 objectInfo.setLastModificationDate(lastModified);
519
520 addPropertyString(properties, typeId, filter, PropertyIds.CHANGE_TOKEN, getChangeToken());
521 }
522
523
524
525
526
527
528
529
530 protected Set<Action> compileAllowableActions(Set<Action> aas) {
531 setAction(aas, Action.CAN_GET_OBJECT_PARENTS, true);
532 setAction(aas, Action.CAN_GET_PROPERTIES, true);
533 setAction(aas, Action.CAN_UPDATE_PROPERTIES, true);
534 setAction(aas, Action.CAN_MOVE_OBJECT, true);
535 setAction(aas, Action.CAN_DELETE_OBJECT, true);
536 setAction(aas, Action.CAN_GET_ACL, false);
537 setAction(aas, Action.CAN_APPLY_ACL, false);
538 setAction(aas, Action.CAN_GET_OBJECT_RELATIONSHIPS, false);
539 setAction(aas, Action.CAN_ADD_OBJECT_TO_FOLDER, false);
540 setAction(aas, Action.CAN_REMOVE_OBJECT_FROM_FOLDER, false);
541 setAction(aas, Action.CAN_APPLY_POLICY, false);
542 setAction(aas, Action.CAN_GET_APPLIED_POLICIES, false);
543 setAction(aas, Action.CAN_REMOVE_POLICY, false);
544 setAction(aas, Action.CAN_CREATE_RELATIONSHIP, false);
545 return aas;
546 }
547
548
549
550
551
552 protected String getChangeToken() throws RepositoryException {
553 return null;
554 }
555
556
557
558
559
560 protected String getLastModifiedBy() throws RepositoryException {
561 return getPropertyOrElse(getContextNode(), Property.JCR_LAST_MODIFIED_BY, USER_UNKNOWN);
562 }
563
564
565
566
567
568 protected GregorianCalendar getLastModified() throws RepositoryException {
569 return getPropertyOrElse(getContextNode(), Property.JCR_LAST_MODIFIED, DATE_UNKNOWN);
570 }
571
572
573
574
575
576 protected GregorianCalendar getCreated() throws RepositoryException {
577 return getPropertyOrElse(getContextNode(), Property.JCR_CREATED, DATE_UNKNOWN);
578 }
579
580
581
582
583
584 protected String getCreatedBy() throws RepositoryException {
585 return getPropertyOrElse(getContextNode(), Property.JCR_CREATED_BY, USER_UNKNOWN);
586 }
587
588
589
590
591
592 protected String getNodeName() throws RepositoryException {
593 return node.getName();
594 }
595
596
597
598
599
600 protected String getObjectId() throws RepositoryException {
601 return getVersionSeriesId();
602 }
603
604
605
606
607
608 protected String getVersionSeriesId() throws RepositoryException {
609 return node.getIdentifier();
610 }
611
612
613
614
615
616
617
618 protected final JcrNode create(Node node) {
619 return typeHandlerManager.create(node);
620 }
621
622
623
624
625 protected final void addPropertyId(PropertiesImpl props, String typeId, Set<String> filter, String id, String value) {
626 if (value == null) {
627 throw new IllegalArgumentException("Value must not be null!");
628 }
629
630 if (!checkAddProperty(props, typeId, filter, id)) {
631 return;
632 }
633
634 PropertyIdImpl prop = new PropertyIdImpl(id, value);
635 prop.setQueryName(id);
636 props.addProperty(prop);
637 }
638
639
640
641
642 protected final void addPropertyString(PropertiesImpl props, String typeId, Set<String> filter, String id, String value) {
643 if (!checkAddProperty(props, typeId, filter, id)) {
644 return;
645 }
646
647 PropertyStringImpl prop = new PropertyStringImpl(id, value);
648 prop.setQueryName(id);
649 props.addProperty(prop);
650 }
651
652
653
654
655 protected final void addPropertyInteger(PropertiesImpl props, String typeId, Set<String> filter, String id, long value) {
656 if (!checkAddProperty(props, typeId, filter, id)) {
657 return;
658 }
659
660 PropertyIntegerImpl prop = new PropertyIntegerImpl(id, BigInteger.valueOf(value));
661 prop.setQueryName(id);
662 props.addProperty(prop);
663 }
664
665
666
667
668 protected final void addPropertyBoolean(PropertiesImpl props, String typeId, Set<String> filter, String id, boolean value) {
669 if (!checkAddProperty(props, typeId, filter, id)) {
670 return;
671 }
672
673 PropertyBooleanImpl prop = new PropertyBooleanImpl(id, value);
674 prop.setQueryName(id);
675 props.addProperty(prop);
676 }
677
678
679
680
681 protected final void addPropertyDateTime(PropertiesImpl props, String typeId, Set<String> filter, String id,
682 GregorianCalendar value) {
683
684 if (!checkAddProperty(props, typeId, filter, id)) {
685 return;
686 }
687
688 PropertyDateTimeImpl prop = new PropertyDateTimeImpl(id, value);
689 prop.setQueryName(id);
690 props.addProperty(prop);
691 }
692
693
694
695
696 protected final boolean checkAddProperty(Properties properties, String typeId, Set<String> filter, String id) {
697 if (properties == null || properties.getProperties() == null) {
698 throw new IllegalArgumentException("Properties must not be null!");
699 }
700
701 if (id == null) {
702 throw new IllegalArgumentException("Id must not be null!");
703 }
704
705 TypeDefinition type = typeManager.getType(typeId);
706 if (type == null) {
707 throw new IllegalArgumentException("Unknown type: " + typeId);
708 }
709 if (!type.getPropertyDefinitions().containsKey(id)) {
710 throw new IllegalArgumentException("Unknown property: " + id);
711 }
712
713 String queryName = type.getPropertyDefinitions().get(id).getQueryName();
714
715 if (queryName != null && filter != null) {
716 if (filter.contains(queryName)) {
717 filter.remove(queryName);
718 }
719 else {
720 return false;
721 }
722 }
723
724 return true;
725 }
726
727
728
729
730 protected static final class PropertyUpdater {
731 private final List<PropertyData<?>> removeProperties = new ArrayList<PropertyData<?>>();
732 private final List<PropertyData<?>> updateProperties = new ArrayList<PropertyData<?>>();
733
734 private PropertyUpdater() { }
735
736 public static PropertyUpdater create(JcrTypeManager typeManager, String typeId, Properties properties) {
737 if (properties == null) {
738 throw new CmisConstraintException("No properties!");
739 }
740
741
742 TypeDefinition type = typeManager.getType(typeId);
743 if (type == null) {
744 throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
745 }
746
747 PropertyUpdater propertyUpdater = new PropertyUpdater();
748
749 for (PropertyData<?> prop : properties.getProperties().values()) {
750 PropertyDefinition<?> propDef = type.getPropertyDefinitions().get(prop.getId());
751
752
753 if (propDef == null) {
754 throw new CmisInvalidArgumentException("Property '" + prop.getId() + "' is unknown!");
755 }
756
757
758 if (propDef.getId().equals(PropertyIds.CONTENT_STREAM_FILE_NAME)) {
759 log.warn("Cannot set " + PropertyIds.CONTENT_STREAM_FILE_NAME + ". Ignoring");
760 continue;
761 }
762
763
764 if (propDef.getId().equals(PropertyIds.NAME)) {
765 continue;
766 }
767
768
769 if (propDef.getUpdatability() == Updatability.READONLY) {
770 throw new CmisConstraintException("Property '" + prop.getId() + "' is readonly!");
771 }
772
773 if (propDef.getUpdatability() == Updatability.ONCREATE) {
774 throw new CmisConstraintException("Property '" + prop.getId() + "' can only be set on create!");
775 }
776
777
778 PropertyData<?> newProp;
779 newProp = PropertyHelper.isPropertyEmpty(prop)
780 ? PropertyHelper.getDefaultValue(propDef)
781 : prop;
782
783
784 if (newProp == null) {
785 propertyUpdater.removeProperties.add(prop);
786 }
787 else {
788 propertyUpdater.updateProperties.add(newProp);
789 }
790 }
791
792 return propertyUpdater;
793 }
794
795 public boolean isEmpty() {
796 return removeProperties.isEmpty() && updateProperties.isEmpty();
797 }
798
799 public void apply(Node node) {
800 try {
801 for (PropertyData<?> prop: removeProperties) {
802 JcrConverter.removeProperty(node, prop);
803 }
804 for (PropertyData<?> prop: updateProperties) {
805 JcrConverter.setProperty(node, prop);
806 }
807 }
808 catch (RepositoryException e) {
809 log.debug(e.getMessage(), e);
810 throw new CmisStorageException(e.getMessage(), e);
811 }
812 }
813 }
814
815
816
817
818 protected final void updateProperties(Node node, String typeId, Properties properties) {
819 PropertyUpdater.create(typeManager, typeId, properties).apply(node);
820 }
821
822
823
824
825
826
827
828
829 protected static VersionHistory getVersionHistory(Node node) throws RepositoryException {
830 return getVersionManager(node).getVersionHistory(node.getPath());
831 }
832
833
834
835
836
837
838
839
840 protected static VersionManager getVersionManager(Node node) throws RepositoryException {
841 return node.getSession().getWorkspace().getVersionManager();
842 }
843
844
845
846
847
848
849
850
851 protected static Version getBaseVersion(Node node) throws RepositoryException {
852 return getVersionManager(node).getBaseVersion(node.getPath());
853 }
854
855
856
857
858
859
860
861
862
863 protected static long getPropertyLength(Node node, String propertyName) throws RepositoryException {
864 return node.hasProperty(propertyName)
865 ? node.getProperty(propertyName).getLength()
866 : -1;
867 }
868
869
870
871
872
873
874
875
876
877
878
879 protected static String getPropertyOrElse(Node node, String propertyName, String defaultValue)
880 throws RepositoryException {
881
882 return node.hasProperty(propertyName)
883 ? node.getProperty(propertyName).getString()
884 : defaultValue;
885 }
886
887
888
889
890
891
892
893
894
895
896
897 protected static GregorianCalendar getPropertyOrElse(Node node, String propertyName, GregorianCalendar defaultValue)
898 throws RepositoryException {
899
900 if (node.hasProperty(propertyName)) {
901 Calendar date = node.getProperty(propertyName).getDate();
902 return Util.toCalendar(date);
903 }
904 else {
905 return defaultValue;
906 }
907 }
908
909
910
911
912
913
914
915
916 protected static void setAction(Set<Action> actions, Action action, boolean condition) {
917 if (condition) {
918 actions.add(action);
919 }
920 else {
921 actions.remove(action);
922 }
923 }
924 }